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

Unified Diff: go/src/infra/tools/cipd/internal/tagcache.go

Issue 1381583007: cipd: Implement local cache for ResolveVersion(...) call with tags. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@cipd-init
Patch Set: Created 5 years, 3 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
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..b588d0a78d3c321f43fae8293f618d79da100c83
--- /dev/null
+++ b/go/src/infra/tools/cipd/internal/tagcache.go
@@ -0,0 +1,174 @@
+// 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,
tandrii(chromium) 2015/09/30 18:11:15 Maybe warn the misuse info to the set-tag and regi
nodir 2015/09/30 19:08:23 We should prevent this from happening on the serve
Vadim Sh. 2015/09/30 22:50:59 Applying same tag to multiple packages is not forb
tandrii_google 2015/09/30 23:11:00 FTR, that's exactly what I intended to do with CQ
nodir 2015/10/01 23:24:04 Looked up the server code: if there is >1 instance
+// 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 interface {
tandrii(chromium) 2015/09/30 18:11:15 frankly, i don't see a reason to have interface =>
nodir 2015/09/30 19:08:24 I agree (with tandrii agreeing with me)
Vadim Sh. 2015/09/30 22:50:59 Converted to struct, whatever. I like interfaces b
tandrii_google 2015/09/30 23:11:00 True, but godoc does look worse. So, it's a trade-
+ // Load loads the state from given buffer.
+ Load([]byte) error
+
+ // Save dumps state to the byte buffer. Also resets 'Dirty' flag.
+ Save() ([]byte, error)
+
+ // Dirty return true if Save() needs to be called to persist changes.
+ Dirty() bool
+
+ // ResolveTag returns cached tag or empty Pin{} if such tag is not in cache.
+ ResolveTag(packageName, tag string) common.Pin
+
+ // AddTag records that (pin.PackageName, tag) maps to pin.InstanceID.
+ AddTag(pin common.Pin, tag string)
+}
+
+// NewTagCache returns implementation of TagCache.
+func NewTagCache() TagCache {
+ return &tagCache{}
+}
+
+// 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 NewTagCache(), nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ cache := NewTagCache()
+ if err := cache.Load(buf); err != nil {
+ return nil, err
+ }
+ return cache, nil
+}
+
+type tagCache struct {
+ lock sync.Mutex
+ cache messages.TagCache
+ dirty bool
+}
+
+func (c *tagCache) Load(buf []byte) error {
+ cache := messages.TagCache{}
+ if err := UnmarshalWithSHA1(buf, &cache); err != nil {
nodir 2015/09/30 19:08:23 Why do you have to do it? A message probably won't
Vadim Sh. 2015/09/30 22:50:59 This code will be used on thousands of bots. Some
nodir 2015/10/01 23:24:03 the word "probably" meant that I don't know for su
Vadim Sh. 2015/10/01 23:34:07 I'm sorry if my reply sounded harsh... Protobufs
+ 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
+}
+
+func (c *tagCache) Save() ([]byte, error) {
+ // Remove all "holes" left from moving entries in AddTag.
+ c.lock.Lock()
+ compacted := make([]*messages.TagCache_Entry, 0, len(c.cache.Entries))
+ for _, e := range c.cache.Entries {
+ if e != nil {
+ compacted = append(compacted, e)
+ }
+ }
tandrii(chromium) 2015/09/30 18:11:15 didn't you mean to add: c.cache.Entries = compac
Vadim Sh. 2015/09/30 22:50:59 Yes, forgot.
+ c.dirty = false
nodir 2015/09/30 19:08:23 set it to false if MarshalWithSHA1 didn't error ou
Vadim Sh. 2015/09/30 22:50:59 Done.
+ c.lock.Unlock()
+
+ // 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.
tandrii(chromium) 2015/09/30 18:11:15 but you don't move "old hot" items to the end, so
Vadim Sh. 2015/09/30 22:50:59 I decided I don't want to write to disk after each
tandrii_google 2015/09/30 23:11:00 Acknowledged.
+ if len(compacted) > MaxTagCacheSize {
+ compacted = compacted[len(compacted)-MaxTagCacheSize:]
+ }
+ return MarshalWithSHA1(&messages.TagCache{Entries: compacted})
+}
+
+func (c *tagCache) Dirty() bool {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ return c.dirty
+}
+
+func (c *tagCache) ResolveTag(pkg, tag string) common.Pin {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, e := range c.cache.Entries {
nodir 2015/09/30 19:08:23 shouldn't you iterate from the tail? given freshes
Vadim Sh. 2015/09/30 22:50:59 Done. Whatever, it is <300 items.
nodir 2015/10/01 23:24:03 I thought that was the point of putting fresh item
Vadim Sh. 2015/10/01 23:34:07 They are in the end because that way no operation
+ if e != nil && e.GetPackage() == pkg && e.GetTag() == tag {
+ return common.Pin{
+ PackageName: pkg,
+ InstanceID: e.GetInstanceId(),
+ }
+ }
+ }
+ return common.Pin{}
+}
+
+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 {
tandrii(chromium) 2015/09/30 18:11:15 since you have comment, why not avoid `bad` var en
nodir 2015/09/30 19:08:23 the if statement will probably become scary
Vadim Sh. 2015/09/30 22:50:59 Yes, 'if' becomes uglier.
+ return
+ }
+
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ // Try to find an existing entry in the cache. It will be moved to bottom
tandrii(chromium) 2015/09/30 18:11:15 this is weird. As a safety check, OK. But current
nodir 2015/09/30 19:08:23 it may happen because of a race condition. State m
Vadim Sh. 2015/09/30 22:50:59 TagCache just implements some interface, regardles
tandrii_google 2015/09/30 23:11:00 I meant it more in connection with compaction not
+ // (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().
tandrii(chromium) 2015/09/30 18:11:15 Hm, holes are compacted in what gets actually writ
nodir 2015/09/30 19:08:23 +1
Vadim Sh. 2015/09/30 22:50:59 Done.
+ 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,
Vadim Sh. 2015/09/30 03:50:00 pointers to strings annoy me, put that's how golan
+ Tag: &tag,
+ }
+ }
+ existing.InstanceId = &pin.InstanceID
+
+ c.dirty = true
+ c.cache.Entries = append(c.cache.Entries, existing)
+}

Powered by Google App Engine
This is Rietveld 408576698