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

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

Powered by Google App Engine
This is Rietveld 408576698