| Index: go/src/infra/tools/cipd/internal/tagcache.go
|
| diff --git a/go/src/infra/tools/cipd/internal/tagcache.go b/go/src/infra/tools/cipd/internal/tagcache.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0ef783daa78990f0a3478cbab12e5154a107b068
|
| --- /dev/null
|
| +++ b/go/src/infra/tools/cipd/internal/tagcache.go
|
| @@ -0,0 +1,164 @@
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package internal
|
| +
|
| +import (
|
| + "io/ioutil"
|
| + "os"
|
| + "sync"
|
| +
|
| + "infra/tools/cipd/common"
|
| + "infra/tools/cipd/internal/messages"
|
| +)
|
| +
|
| +// MaxTagCacheSize is how many entries to keep in TagCache database.
|
| +const MaxTagCacheSize = 300
|
| +
|
| +// TagCache provides a thread safe mapping (package name, tag) -> instance ID.
|
| +// This mapping is safe to cache because tags are not detachable: once a tag is
|
| +// successfully resolved to an instance ID it is guaranteed to resolve to same
|
| +// instance ID later or not resolve at all (e.g. if one tag is attached to
|
| +// multiple instances, in which case the tag is misused anyway). In any case,
|
| +// returning a cached instance ID does make sense. The primary purpose of this
|
| +// cache is to avoid round trips to the service to increase reliability of
|
| +// 'cipd ensure' calls that use only tags to specify versions. It happens to be
|
| +// the most common case of 'cipd ensure' usage by far.
|
| +type TagCache struct {
|
| + lock sync.Mutex
|
| + cache messages.TagCache
|
| + dirty bool
|
| +}
|
| +
|
| +// LoadTagCacheFromFile reads tag cache state from given file path if it exists.
|
| +// Returns empty cache if file doesn't exist.
|
| +func LoadTagCacheFromFile(path string) (*TagCache, error) {
|
| + buf, err := ioutil.ReadFile(path)
|
| + if os.IsNotExist(err) {
|
| + return &TagCache{}, nil
|
| + }
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + cache := &TagCache{}
|
| + if err := cache.Load(buf); err != nil {
|
| + return nil, err
|
| + }
|
| + return cache, nil
|
| +}
|
| +
|
| +// Load loads the state from given buffer.
|
| +func (c *TagCache) Load(buf []byte) error {
|
| + cache := messages.TagCache{}
|
| + if err := UnmarshalWithSHA1(buf, &cache); err != nil {
|
| + return err
|
| + }
|
| +
|
| + // Validate entries. Make sure to keep only MaxTagCacheSize number of them.
|
| + goodOnes := make([]*messages.TagCache_Entry, 0, MaxTagCacheSize)
|
| + for i := 0; i < len(cache.Entries) && len(goodOnes) < MaxTagCacheSize; i++ {
|
| + e := cache.Entries[i]
|
| + valid := e != nil &&
|
| + common.ValidatePackageName(e.GetPackage()) == nil &&
|
| + common.ValidateInstanceTag(e.GetTag()) == nil &&
|
| + common.ValidateInstanceID(e.GetInstanceId()) == nil
|
| + if valid {
|
| + goodOnes = append(goodOnes, e)
|
| + }
|
| + }
|
| +
|
| + c.lock.Lock()
|
| + defer c.lock.Unlock()
|
| + c.cache.Entries = goodOnes
|
| + c.dirty = false
|
| + return nil
|
| +}
|
| +
|
| +// Save dumps state to the byte buffer. Also resets 'Dirty' flag.
|
| +func (c *TagCache) Save() ([]byte, error) {
|
| + c.lock.Lock()
|
| + defer c.lock.Unlock()
|
| +
|
| + // Remove all "holes" left from moving entries in AddTag.
|
| + compacted := make([]*messages.TagCache_Entry, 0, len(c.cache.Entries))
|
| + for _, e := range c.cache.Entries {
|
| + if e != nil {
|
| + compacted = append(compacted, e)
|
| + }
|
| + }
|
| +
|
| + // Keep at most MaxTagCacheSize entries. Truncate head of the slice, since
|
| + // it's where old items are. All new hotness is at the tail, we need
|
| + // to keep it.
|
| + if len(compacted) > MaxTagCacheSize {
|
| + compacted = compacted[len(compacted)-MaxTagCacheSize:]
|
| + }
|
| + c.cache.Entries = compacted
|
| +
|
| + out, err := MarshalWithSHA1(&c.cache)
|
| + if err == nil {
|
| + c.dirty = false
|
| + }
|
| + return out, err
|
| +}
|
| +
|
| +// Dirty returns true if Save() needs to be called to persist changes.
|
| +func (c *TagCache) Dirty() bool {
|
| + c.lock.Lock()
|
| + defer c.lock.Unlock()
|
| + return c.dirty
|
| +}
|
| +
|
| +// ResolveTag returns cached tag or empty Pin{} if such tag is not in cache.
|
| +func (c *TagCache) ResolveTag(pkg, tag string) common.Pin {
|
| + c.lock.Lock()
|
| + defer c.lock.Unlock()
|
| + for i := len(c.cache.Entries) - 1; i >= 0; i-- {
|
| + e := c.cache.Entries[i]
|
| + if e != nil && e.GetPackage() == pkg && e.GetTag() == tag {
|
| + return common.Pin{
|
| + PackageName: pkg,
|
| + InstanceID: e.GetInstanceId(),
|
| + }
|
| + }
|
| + }
|
| + return common.Pin{}
|
| +}
|
| +
|
| +// AddTag records that (pin.PackageName, tag) maps to pin.InstanceID.
|
| +func (c *TagCache) AddTag(pin common.Pin, tag string) {
|
| + // Just skip invalid data. It should not be here anyway.
|
| + bad := common.ValidatePackageName(pin.PackageName) != nil ||
|
| + common.ValidateInstanceID(pin.InstanceID) != nil ||
|
| + common.ValidateInstanceTag(tag) != nil
|
| + if bad {
|
| + return
|
| + }
|
| +
|
| + c.lock.Lock()
|
| + defer c.lock.Unlock()
|
| +
|
| + // Try to find an existing entry in the cache. It will be moved to bottom
|
| + // (thus promoted as "most recent one"). We put a "hole" (nil) in previous
|
| + // position to avoid shifting array for no good reason. All "holes" are
|
| + // compacted in Save().
|
| + var existing *messages.TagCache_Entry
|
| + for i, e := range c.cache.Entries {
|
| + if e != nil && e.GetPackage() == pin.PackageName && e.GetTag() == tag {
|
| + existing = e
|
| + c.cache.Entries[i] = nil
|
| + break
|
| + }
|
| + }
|
| + if existing == nil {
|
| + existing = &messages.TagCache_Entry{
|
| + Package: &pin.PackageName,
|
| + Tag: &tag,
|
| + }
|
| + }
|
| + existing.InstanceId = &pin.InstanceID
|
| +
|
| + c.dirty = true
|
| + c.cache.Entries = append(c.cache.Entries, existing)
|
| +}
|
|
|