Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(57)

Unified Diff: cipd/client/cipd/local/deployer.go

Issue 2640703002: Change CIPD internal pkgs directory layout to use numeric indices. (Closed)
Patch Set: Add tests for numSet, fix bugs found Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « cipd/client/cipd/client.go ('k') | cipd/client/cipd/local/deployer_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: cipd/client/cipd/local/deployer.go
diff --git a/cipd/client/cipd/local/deployer.go b/cipd/client/cipd/local/deployer.go
index f2a1c8b3cdde4f972394126897838bbd66295d6a..85797ecfb95c289b66f3e14f816e3cdd1a49ea1b 100644
--- a/cipd/client/cipd/local/deployer.go
+++ b/cipd/client/cipd/local/deployer.go
@@ -5,16 +5,18 @@
package local
import (
- "crypto/sha1"
- "encoding/base64"
"fmt"
"io/ioutil"
+ "math/rand"
"os"
"path/filepath"
"runtime"
"sort"
+ "strconv"
"strings"
"sync"
+ "syscall"
+ "time"
"golang.org/x/net/context"
@@ -152,7 +154,11 @@ func (d *deployerImpl) DeployInstance(ctx context.Context, inst PackageInstance)
// and files will be moved to the site root later (in addToSiteRoot call).
// ExtractPackageInstance knows how to build full paths and how to atomically
// extract a package. No need to delete garbage if it fails.
- pkgPath := d.packagePath(ctx, pin.PackageName)
+ pkgPath, err := d.packagePath(ctx, pin.PackageName, true)
+ if err != nil {
+ return common.Pin{}, err
+ }
+
destPath := filepath.Join(pkgPath, pin.InstanceID)
if err := ExtractInstance(ctx, inst, NewFileSystemDestination(destPath, d.fs)); err != nil {
return common.Pin{}, err
@@ -235,7 +241,15 @@ func (d *deployerImpl) DeployInstance(ctx context.Context, inst PackageInstance)
}
func (d *deployerImpl) CheckDeployed(ctx context.Context, pkg string) (common.Pin, error) {
- current, err := d.getCurrentInstanceID(d.packagePath(ctx, pkg))
+ pkgPath, err := d.packagePath(ctx, pkg, false)
+ if err != nil {
+ return common.Pin{}, err
+ }
+ if pkgPath == "" {
+ return common.Pin{}, fmt.Errorf("package %s is not installed", pkg)
+ }
+
+ current, err := d.getCurrentInstanceID(pkgPath)
if err != nil {
return common.Pin{}, err
}
@@ -265,22 +279,23 @@ func (d *deployerImpl) FindDeployed(ctx context.Context) ([]common.Pin, error) {
if !info.IsDir() {
continue
}
- // Attempt to read the manifest. If it is there -> valid package is found.
+ // Read the description and the 'current' link.
pkgPath := filepath.Join(pkgs, info.Name())
- currentID, err := d.getCurrentInstanceID(pkgPath)
- if err != nil || currentID == "" {
+ desc, err := d.readDescription(ctx, pkgPath)
+ if err != nil || desc == nil {
continue
}
- manifest, err := d.readManifest(ctx, filepath.Join(pkgPath, currentID))
- if err != nil {
+ currentID, err := d.getCurrentInstanceID(pkgPath)
+ if err != nil || currentID == "" {
continue
}
+
// Ignore duplicate entries, they can appear if someone messes with pkgs/*
// structure manually.
- if _, ok := found[manifest.PackageName]; !ok {
- keys = append(keys, manifest.PackageName)
- found[manifest.PackageName] = common.Pin{
- PackageName: manifest.PackageName,
+ if _, ok := found[desc.PackageName]; !ok {
+ keys = append(keys, desc.PackageName)
+ found[desc.PackageName] = common.Pin{
+ PackageName: desc.PackageName,
InstanceID: currentID,
}
}
@@ -300,7 +315,14 @@ func (d *deployerImpl) RemoveDeployed(ctx context.Context, packageName string) e
if err := common.ValidatePackageName(packageName); err != nil {
return err
}
- pkgPath := d.packagePath(ctx, packageName)
+ pkgPath, err := d.packagePath(ctx, packageName, false)
+ if err != nil {
+ return err
+ }
+ if pkgPath == "" {
+ logging.Warningf(ctx, "Package %s not found", packageName)
+ return nil
+ }
// Read the manifest of the currently installed version.
manifest := Manifest{}
@@ -327,6 +349,14 @@ func (d *deployerImpl) TempFile(ctx context.Context, prefix string) (*os.File, e
return ioutil.TempFile(dir, prefix)
}
+func (d *deployerImpl) TempDir(ctx context.Context, prefix string) (string, error) {
+ dir, err := d.fs.EnsureDirectory(ctx, filepath.Join(d.fs.Root(), SiteServiceDir, "tmp"))
+ if err != nil {
+ return "", err
+ }
+ return ioutil.TempDir(dir, prefix)
+}
+
func (d *deployerImpl) CleanupTrash(ctx context.Context) error {
return d.fs.CleanupTrash(ctx)
}
@@ -334,16 +364,143 @@ func (d *deployerImpl) CleanupTrash(ctx context.Context) error {
////////////////////////////////////////////////////////////////////////////////
// Utility methods.
+type numSet sort.IntSlice
+
+func (s *numSet) addNum(n int) {
+ idx := sort.IntSlice((*s)).Search(n)
Vadim Sh. 2017/01/19 03:02:52 btw, why not container.heap?
+ if idx == len(*s) {
+ // it's insertion point is off the end of the slice
+ *s = append(*s, n)
+ } else if (*s)[idx] != n {
+ // it's insertion point is inside the slice, but is not present.
+ *s = append(*s, 0)
+ copy((*s)[idx+1:], (*s)[idx:])
+ (*s)[idx] = n
+ }
+ // it's already present in the slice
+}
+
+func (s *numSet) smallestNewNum() int {
+ prev := -1
+ for _, n := range *s {
+ if n-1 != prev {
+ return prev + 1
+ }
+ prev = n
+ }
+ return prev + 1
+}
+
// packagePath returns a path to a package directory in .cipd/pkgs/.
-func (d *deployerImpl) packagePath(ctx context.Context, pkg string) string {
- rel := filepath.Join(filepath.FromSlash(packagesDir), packageNameDigest(pkg))
+//
+// This will scan all directories under pkgs, looking for a description.json. If
+// an old-style package folder is encountered (e.g. has an instance folder and
+// current manifest, but doesn't have a description.json), the description.json
+// will be added.
+//
+// If no suitable path is found and allocate is true, this will create a new
+// directory with an accompanying description.json. Otherwise this returns "".
+func (d *deployerImpl) packagePath(ctx context.Context, pkg string, allocate bool) (string, error) {
+ if err := common.ValidatePackageName(pkg); err != nil {
+ return "", err
+ }
+
+ rel := filepath.Join(filepath.FromSlash(packagesDir))
abs, err := d.fs.RootRelToAbs(rel)
if err != nil {
- msg := fmt.Sprintf("can't get absolute path of %q", rel)
- logging.Errorf(ctx, "%s", msg)
- panic(msg)
+ logging.Errorf(ctx, "Can't get absolute path of %q: %s", rel, err)
+ return "", err
+ }
+ files, err := ioutil.ReadDir(abs)
+ if err != nil && !os.IsNotExist(err) {
+ logging.Errorf(ctx, "Can't read packages dir %q: %s", abs, err)
+ return "", err
+ }
+
+ seenNumbers := numSet{}
+
+ for _, f := range files {
+ // keep track of all numeric children of .cipd/pkgs
+ if n, err := strconv.Atoi(f.Name()); err == nil {
+ seenNumbers.addNum(n)
+ }
+
+ fullPkgPath := filepath.Join(abs, f.Name())
+ description, err := d.readDescription(ctx, fullPkgPath)
+ if err != nil {
+ logging.Warningf(ctx, "Skipping %q: %s", fullPkgPath, err)
+ continue
+ }
+ if description.PackageName == pkg {
+ return fullPkgPath, nil
+ }
}
- return abs
+
+ if !allocate {
+ return "", nil
+ }
+
+ // we didn't find one, so we have to make one
+ if _, err := d.fs.EnsureDirectory(ctx, abs); err != nil {
+ logging.Errorf(ctx, "Cannot ensure packages directory: %s", err)
+ return "", err
+ }
+
+ tmpDir, err := d.TempDir(ctx, strings.Replace(pkg, "/", "_", -1))
+ if err != nil {
+ logging.Errorf(ctx, "Cannot create new pkg tempdir: %s", err)
+ return "", err
+ }
+ defer d.fs.EnsureDirectoryGone(ctx, tmpDir)
+ err = d.fs.EnsureFile(ctx, filepath.Join(tmpDir, descriptionName), func(f *os.File) error {
+ return writeDescription(&Description{PackageName: pkg}, f)
+ })
+ if err != nil {
+ logging.Errorf(ctx, "Cannot create new pkg description.json: %s", err)
+ return "", err
+ }
+
+ // now we have to find a suitable index folder for it.
+ for attempts := 0; attempts < 3; attempts++ {
+ if attempts > 0 {
+ // random sleep up to 1s to help avoid collisions between clients.
+ time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
+ }
+ n := seenNumbers.smallestNewNum()
+ seenNumbers.addNum(n)
+
+ pkgPath := filepath.Join(abs, strconv.Itoa(n))
+ // We use os.Rename instead of d.fs.Replace because we want it to fail if
+ // the target directory already exists.
+ switch err := os.Rename(tmpDir, pkgPath); le := err.(type) {
+ case nil:
+ return pkgPath, nil
+
+ case *os.LinkError:
+ if le.Err != syscall.ENOTEMPTY {
+ logging.Errorf(ctx, "Error while creating pkg dir %s: %s", pkgPath, err)
+ return "", err
+ }
+
+ default:
+ logging.Errorf(ctx, "Unknown error while creating pkg dir %s: %s", pkgPath, err)
+ return "", err
+ }
+
+ // rename failed with ENOTEMPTY, that means that another client wrote this
+ // directory.
+ description, err := d.readDescription(ctx, pkgPath)
+ if err != nil {
+ logging.Warningf(ctx, "Skipping %q: %s", pkgPath, err)
+ continue
+ }
+ if description.PackageName == pkg {
+ return pkgPath, nil
+ }
+ }
+
+ logging.Errorf(ctx, "Unable to find valid index for package %q in %s!", pkg, abs)
+ return "", err
}
// getCurrentInstanceID returns instance ID of currently installed instance
@@ -390,6 +547,63 @@ func (d *deployerImpl) setCurrentInstanceID(ctx context.Context, packageDir, ins
return d.fs.EnsureSymlink(ctx, filepath.Join(packageDir, currentSymlink), instanceID)
}
+// readDescription reads the package description.json given a path to a package
+// directory.
+//
+// As a backwards-compatibility measure, it will also upgrade CIPD < 1.4 folders
+// to contain a description.json. Previous to 1.4, package folders only had
+// instance subfolders, and the current instances' manifest was used to
+// determine the package name. Versions prior to 1.4 also installed all packages
+// at the base (root ""), hence the implied root location here.
+//
+// Returns (nil, nil) if no description.json exists and there are no instance
+// folders present.
+func (d *deployerImpl) readDescription(ctx context.Context, pkgDir string) (desc *Description, err error) {
+ descriptionPath := filepath.Join(pkgDir, descriptionName)
+ r, err := os.Open(descriptionPath)
+ switch {
+ case os.IsNotExist(err):
+ // try fixup
+ break
+ case err == nil:
+ defer r.Close()
+ return readDescription(r)
+ default:
+ return
+ }
+
+ // see if this is a pre 1.4 directory
+ currentID, err := d.getCurrentInstanceID(pkgDir)
+ if err != nil {
+ return
+ }
+
+ if currentID == "" {
+ logging.Warningf(ctx, "No current instance id in %s", pkgDir)
+ err = nil
+ return
+ }
+
+ manifest, err := d.readManifest(ctx, filepath.Join(pkgDir, currentID))
+ if err != nil {
+ return
+ }
+
+ desc = &Description{
+ PackageName: manifest.PackageName,
+ }
+ // To handle the case where some other user owns these directories, all errors
+ // from here to the end are treated as warnings.
+ err = d.fs.EnsureFile(ctx, descriptionPath, func(f *os.File) error {
+ return writeDescription(desc, f)
+ })
+ if err != nil {
+ logging.Warningf(ctx, "Unable to create description.json: %s", err)
+ err = nil
+ }
+ return
+}
+
// readManifest reads package manifest given a path to a package instance
// (.cipd/pkgs/<name>/<instance id>).
func (d *deployerImpl) readManifest(ctx context.Context, instanceDir string) (Manifest, error) {
@@ -496,39 +710,6 @@ func (d *deployerImpl) removeEmptyDirs(ctx context.Context, root string) {
////////////////////////////////////////////////////////////////////////////////
// Utility functions.
-// packageNameDigest returns a filename to use for naming a package directory in
-// the file system. Using package names as is can introduce problems on file
-// systems with path length limits (on Windows in particular). Returns stripped
-// SHA1 of the whole package name on Windows. On Linux\Mac also prepends last
-// two components of the package name (for better readability of .cipd/*
-// directory).
-func packageNameDigest(pkg string) string {
- // Be paranoid.
- err := common.ValidatePackageName(pkg)
- if err != nil {
- panic(err.Error())
- }
-
- // Grab stripped SHA1 of the full package name.
- digest := sha1.Sum([]byte(pkg))
- hash := base64.URLEncoding.EncodeToString(digest[:])[:10]
-
- // On Windows paths are restricted to 260 chars, so every byte counts.
- if runtime.GOOS == "windows" {
- return hash
- }
-
- // On Posix file paths are not so restricted, so we can make names more
- // readable. Grab last <= 2 components of the package path and join them with
- // the digest.
- chunks := strings.Split(pkg, "/")
- if len(chunks) > 2 {
- chunks = chunks[len(chunks)-2:]
- }
- chunks = append(chunks, hash)
- return strings.Join(chunks, "_")
-}
-
// scanPackageDir finds a set of regular files (and symlinks) in a package
// instance directory and returns them as FileInfo structs (with slash-separated
// paths relative to dir directory). Skips package service directories (.cipdpkg
« no previous file with comments | « cipd/client/cipd/client.go ('k') | cipd/client/cipd/local/deployer_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698