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

Side by Side Diff: client/cipd/internal/instancecache.go

Issue 1870263002: cipd: instance cache (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-go@master
Patch Set: addressed comments Created 4 years, 8 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package internal
6
7 import (
8 "bytes"
9 "fmt"
10 "io"
11 "io/ioutil"
12 "math/rand"
13 "os"
14 "sync"
15 "time"
16
17 "github.com/luci/luci-go/common/logging"
18 "github.com/luci/luci-go/common/proto/google"
19 "github.com/luci/luci-go/common/stringset"
20
21 "github.com/luci/luci-go/client/cipd/common"
22 "github.com/luci/luci-go/client/cipd/internal/messages"
23 "github.com/luci/luci-go/client/cipd/local"
24 )
25
26 const (
27 instanceCacheMaxSize = 100
28 // instanceCacheSyncInterval determines the frequency of
29 // synchronization of state.db with files in the cache dir.
30 instanceCacheSyncInterval = 8 * time.Hour
31 instanceCacheStateFilename = "state.db"
32 )
33
34 // InstanceCache is a file-system-based, thread-safe, LRU cache of instances.
35 //
36 // Does not validate instance hashes. It is caller responsibility to do so.
37 type InstanceCache struct {
38 fs local.FileSystem
39 lock sync.Mutex
40 logger logging.Logger
41 state messages.InstanceCache
42 dirty bool
43 }
44
45 // LoadInstanceCache initializes InstanceCache.
46 // fs will be root of the instance cache.
47 func LoadInstanceCache(fs local.FileSystem, logger logging.Logger, now time.Time ) *InstanceCache {
48 if logger == nil {
49 logger = logging.Null()
50 }
51 cache := &InstanceCache{
52 fs: fs,
53 logger: logger,
54 }
55 cache.readState(now)
56 return cache
57 }
58
59 // Get searches for the instance in cache and writes its contents to output.
60 // If the instance is not found, returns an os.IsNotExists error without writing
61 // to output.
62 func (c *InstanceCache) Get(pin common.Pin, output io.Writer, now time.Time) err or {
63 c.lock.Lock()
64 defer c.lock.Unlock()
65
66 path, err := c.fs.RootRelToAbs(pin.InstanceID)
67 if err != nil {
68 return fmt.Errorf("invalid instance ID %q", pin.InstanceID)
69 }
70
71 f, err := os.Open(path)
72 if err != nil {
73 if os.IsNotExist(err) {
74 delete(c.state.Entries, pin.InstanceID)
75 }
76 return err
77 }
78 defer f.Close()
79
80 c.touch(pin.InstanceID, now)
81
82 _, err = io.Copy(output, f)
83 return err
84 }
85
86 // Put caches instance contents.
87 // May remove some packages that were not accessed for a long time.
88 func (c *InstanceCache) Put(pin common.Pin, now time.Time, write func(*os.File) error) error {
89 c.lock.Lock()
90 defer c.lock.Unlock()
91
92 c.touch(pin.InstanceID, now)
93
94 if err := c.fs.EnsureFile(pin.InstanceID, write); err != nil {
95 return err
96 }
97
98 c.gc()
99 return nil
100 }
101
102 // Save persists cache state to the state file.
103 func (c *InstanceCache) Save() error {
104 return c.saveState()
105 }
106
107 // Dirty returns true if cache state differs from the state file.
108 func (c *InstanceCache) Dirty() bool {
109 return c.dirty
110 }
111
112 // gc checks if the number of instances in the state is greater than maximum.
113 // If yes, purges excessive oldest instances.
114 func (c *InstanceCache) gc() {
115 n := len(c.state.Entries)
116 m := instanceCacheMaxSize
117 if n <= m {
118 return
119 }
120
121 // Among all instances, leave only top m recently-accessed.
122 // Find the m-th recently-accessed instance.
123 found := 0
124 var cutOff time.Time // lastAccess of found-th most-recently-accessed in stance
125 for _, s := range c.state.Entries {
126 lastAccess := s.LastAccess.Time()
127 switch {
128 case found == 0:
129 cutOff = lastAccess
130 found++
131
132 case found < m:
133 if lastAccess.Before(cutOff) {
134 cutOff = lastAccess
135 }
136 found++
137
138 case found == m:
139 if lastAccess.After(cutOff) {
140 cutOff = lastAccess
141 }
142 }
143 }
144
145 // First m-n instances that were last accessed before cutOff are garbage .
146 garbage := make([]string, 0, m-n)
147 // Map iteration is not deterministic, but it is fine.
148 for id, e := range c.state.Entries {
149 if e.LastAccess.Time().Before(cutOff) {
150 garbage = append(garbage, id)
151 if len(garbage) == cap(garbage) {
152 break
153 }
154 }
155 }
156
157 for _, id := range garbage {
158 path, err := c.fs.RootRelToAbs(id)
159 if err != nil {
160 panic("impossible")
161 }
162 if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
163 c.logger.Errorf("cipd: could not delete %s from cache - %s", path, err)
164 }
165 delete(c.state.Entries, id)
166 }
167 }
168
169 // readState loads c.state from the state file.
170 // If the file does not exist, corrupted or its state was not synchronized
171 // with the instance files for a long time, synchronizes it.
172 // Newly discovered files are considered last accessed at EPOCH.
173 func (c *InstanceCache) readState(now time.Time) error {
174 statePath, err := c.fs.RootRelToAbs(instanceCacheStateFilename)
175 if err != nil {
176 panic("impossible")
177 }
178
179 stateBytes, err := ioutil.ReadFile(statePath)
180 sync := false
181 switch {
182 case os.IsNotExist(err):
183 sync = true
184
185 case err != nil:
186 c.logger.Errorf("cipd: could not read instance cache - %s", err)
187 sync = true
188
189 default:
190 if err := UnmarshalWithSHA1(stateBytes, &c.state); err != nil {
191 c.logger.Errorf("cipd: instance cache file is corrupted - %s", err)
192 c.state = messages.InstanceCache{}
193 sync = true
194 } else {
195 cutOff := now.
196 Add(-instanceCacheSyncInterval).
197 Add(time.Duration(rand.Int63n(int64(5 * time.Min ute))))
198 sync = c.state.LastSynced.Time().Before(cutOff)
199 }
200 }
201
202 if sync {
203 if err := c.syncState(now); err != nil {
204 c.logger.Errorf("cipd: failed to sync instance cache - % s", err)
205 }
206 c.gc()
207 }
208 return nil
209 }
210
211 // syncState synchronizes the list of instances with instance files.
212 // Preserves lastAccess of existing instances. Newly discovered files are
213 // considered last accessed at EPOCH.
214 func (c *InstanceCache) syncState(now time.Time) error {
215 root, err := os.Open(c.fs.Root())
216 switch {
217
218 case os.IsNotExist(err):
219 c.state.Entries = nil
220
221 case err != nil:
222 return err
223
224 default:
225 instanceIDs, err := root.Readdirnames(0)
226 if err != nil {
227 return err
228 }
229 existingIDs := stringset.New(len(instanceIDs))
230 for _, id := range instanceIDs {
231 if common.ValidateInstanceID(id) != nil {
232 continue
233 }
234 existingIDs.Add(id)
235
236 if _, ok := c.state.Entries[id]; !ok {
237 c.ensureEntries()
238 c.state.Entries[id] = &messages.InstanceCache_En try{}
239 }
240 }
241
242 for id := range c.state.Entries {
243 if !existingIDs.Has(id) {
244 delete(c.state.Entries, id)
245 }
246 }
247 }
248
249 c.state.LastSynced = google.NewTimestamp(now)
250 c.dirty = true
251 return nil
252 }
253
254 // saveState persists the cache state.
255 func (c *InstanceCache) saveState() error {
256 stateBytes, err := MarshalWithSHA1(&c.state)
257 if err != nil {
258 return err
259 }
260
261 if err := local.EnsureFile(c.fs, instanceCacheStateFilename, bytes.NewRe ader(stateBytes)); err != nil {
262 return err
263 }
264
265 c.dirty = false
266 return nil
267 }
268
269 func (c *InstanceCache) ensureEntries() {
270 if c.state.Entries == nil {
271 c.state.Entries = map[string]*messages.InstanceCache_Entry{}
272 }
273 }
274
275 // touch updates/adds last access time for an instance.
276 func (c *InstanceCache) touch(instanceID string, now time.Time) {
277 entry := c.state.Entries[instanceID]
278 if entry == nil {
279 entry = &messages.InstanceCache_Entry{}
280 c.ensureEntries()
281 c.state.Entries[instanceID] = entry
282 }
283 entry.LastAccess = google.NewTimestamp(now)
284 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698