21700: Install Bundler system-wide in Rails postinst
[arvados.git] / sdk / go / arvados / keep_cache.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "bytes"
9         "context"
10         "crypto/md5"
11         "errors"
12         "fmt"
13         "io"
14         "io/fs"
15         "os"
16         "path/filepath"
17         "sort"
18         "strconv"
19         "strings"
20         "sync"
21         "sync/atomic"
22         "syscall"
23         "time"
24
25         "github.com/sirupsen/logrus"
26         "golang.org/x/sys/unix"
27 )
28
29 type KeepGateway interface {
30         ReadAt(locator string, dst []byte, offset int) (int, error)
31         BlockRead(ctx context.Context, opts BlockReadOptions) (int, error)
32         BlockWrite(ctx context.Context, opts BlockWriteOptions) (BlockWriteResponse, error)
33         LocalLocator(locator string) (string, error)
34 }
35
36 // DiskCache wraps KeepGateway, adding a disk-based cache layer.
37 //
38 // A DiskCache is automatically incorporated into the backend stack of
39 // each keepclient.KeepClient. Most programs do not need to use
40 // DiskCache directly.
41 type DiskCache struct {
42         KeepGateway
43         Dir     string
44         MaxSize ByteSizeOrPercent
45         Logger  logrus.FieldLogger
46
47         *sharedCache
48         setupOnce sync.Once
49 }
50
51 var (
52         sharedCachesLock sync.Mutex
53         sharedCaches     = map[string]*sharedCache{}
54 )
55
56 // sharedCache has fields that coordinate the cache usage in a single
57 // cache directory; it can be shared by multiple DiskCaches.
58 //
59 // This serves to share a single pool of held-open filehandles, a
60 // single tidying goroutine, etc., even when the program (like
61 // keep-web) uses multiple KeepGateway stacks that use different auth
62 // tokens, etc.
63 type sharedCache struct {
64         dir     string
65         maxSize ByteSizeOrPercent
66
67         tidying        int32 // see tidy()
68         defaultMaxSize int64
69
70         // The "heldopen" fields are used to open cache files for
71         // reading, and leave them open for future/concurrent ReadAt
72         // operations. See quickReadAt.
73         heldopen     map[string]*openFileEnt
74         heldopenMax  int
75         heldopenLock sync.Mutex
76
77         // The "writing" fields allow multiple concurrent/sequential
78         // ReadAt calls to be notified as a single
79         // read-block-from-backend-into-cache goroutine fills the
80         // cache file.
81         writing     map[string]*writeprogress
82         writingCond *sync.Cond
83         writingLock sync.Mutex
84
85         sizeMeasured    int64 // actual size on disk after last tidy(); zero if not measured yet
86         sizeEstimated   int64 // last measured size, plus files we have written since
87         lastFileCount   int64 // number of files on disk at last count
88         writesSinceTidy int64 // number of files written since last tidy()
89 }
90
91 type writeprogress struct {
92         cond    *sync.Cond     // broadcast whenever size or done changes
93         done    bool           // size and err have their final values
94         size    int            // bytes copied into cache file so far
95         err     error          // error encountered while copying from backend to cache
96         sharedf *os.File       // readable filehandle, usable if done && err==nil
97         readers sync.WaitGroup // goroutines that haven't finished reading from f yet
98 }
99
100 type openFileEnt struct {
101         sync.RWMutex
102         f   *os.File
103         err error // if err is non-nil, f should not be used.
104 }
105
106 const (
107         cacheFileSuffix = ".keepcacheblock"
108         tmpFileSuffix   = ".tmp"
109 )
110
111 func (cache *DiskCache) setup() {
112         sharedCachesLock.Lock()
113         defer sharedCachesLock.Unlock()
114         dir := cache.Dir
115         if sharedCaches[dir] == nil {
116                 sharedCaches[dir] = &sharedCache{dir: dir, maxSize: cache.MaxSize}
117         }
118         cache.sharedCache = sharedCaches[dir]
119 }
120
121 func (cache *DiskCache) cacheFile(locator string) string {
122         hash := locator
123         if i := strings.Index(hash, "+"); i > 0 {
124                 hash = hash[:i]
125         }
126         return filepath.Join(cache.dir, hash[:3], hash+cacheFileSuffix)
127 }
128
129 // Open a cache file, creating the parent dir if necessary.
130 func (cache *DiskCache) openFile(name string, flags int) (*os.File, error) {
131         f, err := os.OpenFile(name, flags, 0600)
132         if os.IsNotExist(err) {
133                 // Create the parent dir and try again. (We could have
134                 // checked/created the parent dir before, but that
135                 // would be less efficient in the much more common
136                 // situation where it already exists.)
137                 parent, _ := filepath.Split(name)
138                 os.Mkdir(parent, 0700)
139                 f, err = os.OpenFile(name, flags, 0600)
140         }
141         return f, err
142 }
143
144 // Rename a file, creating the new path's parent dir if necessary.
145 func (cache *DiskCache) rename(old, new string) error {
146         if nil == os.Rename(old, new) {
147                 return nil
148         }
149         parent, _ := filepath.Split(new)
150         os.Mkdir(parent, 0700)
151         return os.Rename(old, new)
152 }
153
154 func (cache *DiskCache) debugf(format string, args ...interface{}) {
155         logger := cache.Logger
156         if logger == nil {
157                 return
158         }
159         logger.Debugf(format, args...)
160 }
161
162 // BlockWrite writes through to the wrapped KeepGateway, and (if
163 // possible) retains a copy of the written block in the cache.
164 func (cache *DiskCache) BlockWrite(ctx context.Context, opts BlockWriteOptions) (BlockWriteResponse, error) {
165         cache.setupOnce.Do(cache.setup)
166         unique := fmt.Sprintf("%x.%p%s", os.Getpid(), &opts, tmpFileSuffix)
167         tmpfilename := filepath.Join(cache.dir, "tmp", unique)
168         tmpfile, err := cache.openFile(tmpfilename, os.O_CREATE|os.O_EXCL|os.O_RDWR)
169         if err != nil {
170                 cache.debugf("BlockWrite: open(%s) failed: %s", tmpfilename, err)
171                 return cache.KeepGateway.BlockWrite(ctx, opts)
172         }
173
174         ctx, cancel := context.WithCancel(ctx)
175         defer cancel()
176         copyerr := make(chan error, 1)
177
178         // Start a goroutine to copy the caller's source data to
179         // tmpfile, a hash checker, and (via pipe) the wrapped
180         // KeepGateway.
181         pipereader, pipewriter := io.Pipe()
182         defer pipereader.Close()
183         go func() {
184                 // Note this is a double-close (which is a no-op) in
185                 // the happy path.
186                 defer tmpfile.Close()
187                 // Note this is a no-op in the happy path (the
188                 // uniquely named tmpfilename will have been renamed).
189                 defer os.Remove(tmpfilename)
190                 defer pipewriter.Close()
191
192                 // Copy from opts.Data or opts.Reader, depending on
193                 // which was provided.
194                 var src io.Reader
195                 if opts.Data != nil {
196                         src = bytes.NewReader(opts.Data)
197                 } else {
198                         src = opts.Reader
199                 }
200
201                 hashcheck := md5.New()
202                 n, err := io.Copy(io.MultiWriter(tmpfile, pipewriter, hashcheck), src)
203                 if err != nil {
204                         copyerr <- err
205                         cancel()
206                         return
207                 } else if opts.DataSize > 0 && opts.DataSize != int(n) {
208                         copyerr <- fmt.Errorf("block size %d did not match provided size %d", n, opts.DataSize)
209                         cancel()
210                         return
211                 }
212                 err = tmpfile.Close()
213                 if err != nil {
214                         // Don't rename tmpfile into place, but allow
215                         // the BlockWrite call to succeed if nothing
216                         // else goes wrong.
217                         return
218                 }
219                 hash := fmt.Sprintf("%x", hashcheck.Sum(nil))
220                 if opts.Hash != "" && opts.Hash != hash {
221                         // Even if the wrapped KeepGateway doesn't
222                         // notice a problem, this should count as an
223                         // error.
224                         copyerr <- fmt.Errorf("block hash %s did not match provided hash %s", hash, opts.Hash)
225                         cancel()
226                         return
227                 }
228                 cachefilename := cache.cacheFile(hash)
229                 err = cache.rename(tmpfilename, cachefilename)
230                 if err != nil {
231                         cache.debugf("BlockWrite: rename(%s, %s) failed: %s", tmpfilename, cachefilename, err)
232                 }
233                 atomic.AddInt64(&cache.sizeEstimated, int64(n))
234                 cache.gotidy()
235         }()
236
237         // Write through to the wrapped KeepGateway from the pipe,
238         // instead of the original reader.
239         newopts := opts
240         if newopts.DataSize == 0 {
241                 newopts.DataSize = len(newopts.Data)
242         }
243         newopts.Reader = pipereader
244         newopts.Data = nil
245
246         resp, err := cache.KeepGateway.BlockWrite(ctx, newopts)
247         if len(copyerr) > 0 {
248                 // If the copy-to-pipe goroutine failed, that error
249                 // will be more helpful than the resulting "context
250                 // canceled" or "read [from pipereader] failed" error
251                 // seen by the wrapped KeepGateway.
252                 //
253                 // If the wrapped KeepGateway encounters an error
254                 // before all the data is copied into the pipe, it
255                 // stops reading from the pipe, which causes the
256                 // io.Copy() in the goroutine to block until our
257                 // deferred pipereader.Close() call runs. In that case
258                 // len(copyerr)==0 here, so the wrapped KeepGateway
259                 // error is the one we return to our caller.
260                 err = <-copyerr
261         }
262         return resp, err
263 }
264
265 type funcwriter func([]byte) (int, error)
266
267 func (fw funcwriter) Write(p []byte) (int, error) {
268         return fw(p)
269 }
270
271 // ReadAt reads the entire block from the wrapped KeepGateway into the
272 // cache if needed, and copies the requested portion into the provided
273 // slice.
274 //
275 // ReadAt returns as soon as the requested portion is available in the
276 // cache. The remainder of the block may continue to be copied into
277 // the cache in the background.
278 func (cache *DiskCache) ReadAt(locator string, dst []byte, offset int) (int, error) {
279         cache.setupOnce.Do(cache.setup)
280         cachefilename := cache.cacheFile(locator)
281         if n, err := cache.quickReadAt(cachefilename, dst, offset); err == nil {
282                 return n, nil
283         }
284
285         cache.writingLock.Lock()
286         progress := cache.writing[cachefilename]
287         if progress == nil {
288                 // Nobody else is fetching from backend, so we'll add
289                 // a new entry to cache.writing, fetch in a separate
290                 // goroutine.
291                 progress = &writeprogress{}
292                 progress.cond = sync.NewCond(&sync.Mutex{})
293                 if cache.writing == nil {
294                         cache.writing = map[string]*writeprogress{}
295                 }
296                 cache.writing[cachefilename] = progress
297
298                 // Start a goroutine to copy from backend to f. As
299                 // data arrives, wake up any waiting loops (see below)
300                 // so ReadAt() requests for partial data can return as
301                 // soon as the relevant bytes have been copied.
302                 go func() {
303                         var size int
304                         var err error
305                         defer func() {
306                                 if err == nil && progress.sharedf != nil {
307                                         err = progress.sharedf.Sync()
308                                 }
309                                 progress.cond.L.Lock()
310                                 progress.err = err
311                                 progress.done = true
312                                 progress.size = size
313                                 progress.cond.L.Unlock()
314                                 progress.cond.Broadcast()
315                                 cache.writingLock.Lock()
316                                 delete(cache.writing, cachefilename)
317                                 cache.writingLock.Unlock()
318
319                                 // Wait for other goroutines to wake
320                                 // up, notice we're done, and use our
321                                 // sharedf to read their data, before
322                                 // we close sharedf.
323                                 //
324                                 // Nobody can join the WaitGroup after
325                                 // the progress entry is deleted from
326                                 // cache.writing above. Therefore,
327                                 // this Wait ensures nobody else is
328                                 // accessing progress, and we don't
329                                 // need to lock anything.
330                                 progress.readers.Wait()
331                                 progress.sharedf.Close()
332                         }()
333                         progress.sharedf, err = cache.openFile(cachefilename, os.O_CREATE|os.O_RDWR)
334                         if err != nil {
335                                 err = fmt.Errorf("ReadAt: %w", err)
336                                 return
337                         }
338                         err = syscall.Flock(int(progress.sharedf.Fd()), syscall.LOCK_SH)
339                         if err != nil {
340                                 err = fmt.Errorf("flock(%s, lock_sh) failed: %w", cachefilename, err)
341                                 return
342                         }
343                         size, err = cache.KeepGateway.BlockRead(context.Background(), BlockReadOptions{
344                                 Locator: locator,
345                                 WriteTo: funcwriter(func(p []byte) (int, error) {
346                                         n, err := progress.sharedf.Write(p)
347                                         if n > 0 {
348                                                 progress.cond.L.Lock()
349                                                 progress.size += n
350                                                 progress.cond.L.Unlock()
351                                                 progress.cond.Broadcast()
352                                         }
353                                         return n, err
354                                 })})
355                         atomic.AddInt64(&cache.sizeEstimated, int64(size))
356                         cache.gotidy()
357                 }()
358         }
359         // We add ourselves to the readers WaitGroup so the
360         // fetch-from-backend goroutine doesn't close the shared
361         // filehandle before we read the data we need from it.
362         progress.readers.Add(1)
363         defer progress.readers.Done()
364         cache.writingLock.Unlock()
365
366         progress.cond.L.Lock()
367         for !progress.done && progress.size < len(dst)+offset {
368                 progress.cond.Wait()
369         }
370         sharedf := progress.sharedf
371         err := progress.err
372         progress.cond.L.Unlock()
373
374         if err != nil {
375                 // If the copy-from-backend goroutine encountered an
376                 // error, we return that error. (Even if we read the
377                 // desired number of bytes, the error might be
378                 // something like BadChecksum so we should not ignore
379                 // it.)
380                 return 0, err
381         }
382         if len(dst) == 0 {
383                 // It's possible that sharedf==nil here (the writer
384                 // goroutine might not have done anything at all yet)
385                 // and we don't need it anyway because no bytes are
386                 // being read. Reading zero bytes seems pointless, but
387                 // if someone does it, we might as well return
388                 // suitable values, rather than risk a crash by
389                 // calling sharedf.ReadAt() when sharedf is nil.
390                 return 0, nil
391         }
392         return sharedf.ReadAt(dst, int64(offset))
393 }
394
395 var quickReadAtLostRace = errors.New("quickReadAt: lost race")
396
397 // Remove the cache entry for the indicated cachefilename if it
398 // matches expect (quickReadAt() usage), or if expect is nil (tidy()
399 // usage).
400 //
401 // If expect is non-nil, close expect's filehandle.
402 //
403 // If expect is nil and a different cache entry is deleted, close its
404 // filehandle.
405 func (cache *DiskCache) deleteHeldopen(cachefilename string, expect *openFileEnt) {
406         needclose := expect
407
408         cache.heldopenLock.Lock()
409         found := cache.heldopen[cachefilename]
410         if found != nil && (expect == nil || expect == found) {
411                 delete(cache.heldopen, cachefilename)
412                 needclose = found
413         }
414         cache.heldopenLock.Unlock()
415
416         if needclose != nil {
417                 needclose.Lock()
418                 defer needclose.Unlock()
419                 if needclose.f != nil {
420                         needclose.f.Close()
421                         needclose.f = nil
422                 }
423         }
424 }
425
426 // quickReadAt attempts to use a cached-filehandle approach to read
427 // from the indicated file. The expectation is that the caller
428 // (ReadAt) will try a more robust approach when this fails, so
429 // quickReadAt doesn't try especially hard to ensure success in
430 // races. In particular, when there are concurrent calls, and one
431 // fails, that can cause others to fail too.
432 func (cache *DiskCache) quickReadAt(cachefilename string, dst []byte, offset int) (int, error) {
433         isnew := false
434         cache.heldopenLock.Lock()
435         if cache.heldopenMax == 0 {
436                 // Choose a reasonable limit on open cache files based
437                 // on RLIMIT_NOFILE. Note Go automatically raises
438                 // softlimit to hardlimit, so it's typically 1048576,
439                 // not 1024.
440                 lim := syscall.Rlimit{}
441                 err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
442                 if err != nil {
443                         cache.heldopenMax = 100
444                 } else if lim.Cur > 400000 {
445                         cache.heldopenMax = 10000
446                 } else {
447                         cache.heldopenMax = int(lim.Cur / 40)
448                 }
449         }
450         heldopen := cache.heldopen[cachefilename]
451         if heldopen == nil {
452                 isnew = true
453                 heldopen = &openFileEnt{}
454                 if cache.heldopen == nil {
455                         cache.heldopen = make(map[string]*openFileEnt, cache.heldopenMax)
456                 } else if len(cache.heldopen) > cache.heldopenMax {
457                         // Rather than go to the trouble of tracking
458                         // last access time, just close all files, and
459                         // open again as needed. Even in the worst
460                         // pathological case, this causes one extra
461                         // open+close per read, which is not
462                         // especially bad (see benchmarks).
463                         go func(m map[string]*openFileEnt) {
464                                 for _, heldopen := range m {
465                                         heldopen.Lock()
466                                         defer heldopen.Unlock()
467                                         if heldopen.f != nil {
468                                                 heldopen.f.Close()
469                                                 heldopen.f = nil
470                                         }
471                                 }
472                         }(cache.heldopen)
473                         cache.heldopen = nil
474                 }
475                 cache.heldopen[cachefilename] = heldopen
476                 heldopen.Lock()
477         }
478         cache.heldopenLock.Unlock()
479
480         if isnew {
481                 // Open and flock the file, save the filehandle (or
482                 // error) in heldopen.f, and release the write lock so
483                 // other goroutines waiting at heldopen.RLock() below
484                 // can use the shared filehandle (or shared error).
485                 f, err := os.Open(cachefilename)
486                 if err == nil {
487                         err = syscall.Flock(int(f.Fd()), syscall.LOCK_SH)
488                         if err == nil {
489                                 heldopen.f = f
490                         } else {
491                                 f.Close()
492                         }
493                 }
494                 if err != nil {
495                         heldopen.err = err
496                         go cache.deleteHeldopen(cachefilename, heldopen)
497                 }
498                 heldopen.Unlock()
499         }
500         // Acquire read lock to ensure (1) initialization is complete,
501         // if it's done by a different goroutine, and (2) any "delete
502         // old/unused entries" waits for our read to finish before
503         // closing the file.
504         heldopen.RLock()
505         defer heldopen.RUnlock()
506         if heldopen.err != nil {
507                 // Other goroutine encountered an error during setup
508                 return 0, heldopen.err
509         } else if heldopen.f == nil {
510                 // Other goroutine closed the file before we got RLock
511                 return 0, quickReadAtLostRace
512         }
513
514         // If another goroutine is currently writing the file, wait
515         // for it to catch up to the end of the range we need.
516         cache.writingLock.Lock()
517         progress := cache.writing[cachefilename]
518         cache.writingLock.Unlock()
519         if progress != nil {
520                 progress.cond.L.Lock()
521                 for !progress.done && progress.size < len(dst)+offset {
522                         progress.cond.Wait()
523                 }
524                 progress.cond.L.Unlock()
525                 // If size<needed && progress.err!=nil here, we'll end
526                 // up reporting a less helpful "EOF reading from cache
527                 // file" below, instead of the actual error fetching
528                 // from upstream to cache file.  This is OK though,
529                 // because our caller (ReadAt) doesn't even report our
530                 // error, it just retries.
531         }
532
533         n, err := heldopen.f.ReadAt(dst, int64(offset))
534         if err != nil {
535                 // wait for any concurrent users to finish, then
536                 // delete this cache entry in case reopening the
537                 // backing file helps.
538                 go cache.deleteHeldopen(cachefilename, heldopen)
539         }
540         return n, err
541 }
542
543 // BlockRead reads an entire block using a 128 KiB buffer.
544 func (cache *DiskCache) BlockRead(ctx context.Context, opts BlockReadOptions) (int, error) {
545         cache.setupOnce.Do(cache.setup)
546         i := strings.Index(opts.Locator, "+")
547         if i < 0 || i >= len(opts.Locator) {
548                 return 0, errors.New("invalid block locator: no size hint")
549         }
550         sizestr := opts.Locator[i+1:]
551         i = strings.Index(sizestr, "+")
552         if i > 0 {
553                 sizestr = sizestr[:i]
554         }
555         blocksize, err := strconv.ParseInt(sizestr, 10, 32)
556         if err != nil || blocksize < 0 {
557                 return 0, errors.New("invalid block locator: invalid size hint")
558         }
559
560         offset := 0
561         buf := make([]byte, 131072)
562         for offset < int(blocksize) {
563                 if ctx.Err() != nil {
564                         return offset, ctx.Err()
565                 }
566                 if int(blocksize)-offset < len(buf) {
567                         buf = buf[:int(blocksize)-offset]
568                 }
569                 nr, err := cache.ReadAt(opts.Locator, buf, offset)
570                 if nr > 0 {
571                         nw, err := opts.WriteTo.Write(buf[:nr])
572                         if err != nil {
573                                 return offset + nw, err
574                         }
575                 }
576                 offset += nr
577                 if err != nil {
578                         return offset, err
579                 }
580         }
581         return offset, nil
582 }
583
584 // Start a tidy() goroutine, unless one is already running / recently
585 // finished.
586 func (cache *DiskCache) gotidy() {
587         writes := atomic.AddInt64(&cache.writesSinceTidy, 1)
588         // Skip if another tidy goroutine is running in this process.
589         n := atomic.AddInt32(&cache.tidying, 1)
590         if n != 1 {
591                 atomic.AddInt32(&cache.tidying, -1)
592                 return
593         }
594         // Skip if sizeEstimated is based on an actual measurement and
595         // is below maxSize, and we haven't done very many writes
596         // since last tidy (defined as 1% of number of cache files at
597         // last count).
598         if cache.sizeMeasured > 0 &&
599                 atomic.LoadInt64(&cache.sizeEstimated) < atomic.LoadInt64(&cache.defaultMaxSize) &&
600                 writes < cache.lastFileCount/100 {
601                 atomic.AddInt32(&cache.tidying, -1)
602                 return
603         }
604         go func() {
605                 cache.tidy()
606                 atomic.StoreInt64(&cache.writesSinceTidy, 0)
607                 atomic.AddInt32(&cache.tidying, -1)
608         }()
609 }
610
611 // Delete cache files as needed to control disk usage.
612 func (cache *DiskCache) tidy() {
613         maxsize := int64(cache.maxSize.ByteSize())
614         if maxsize < 1 {
615                 maxsize = atomic.LoadInt64(&cache.defaultMaxSize)
616                 if maxsize == 0 {
617                         // defaultMaxSize not yet computed. Use 10% of
618                         // filesystem capacity (or different
619                         // percentage if indicated by cache.maxSize)
620                         pct := cache.maxSize.Percent()
621                         if pct == 0 {
622                                 pct = 10
623                         }
624                         var stat unix.Statfs_t
625                         if nil == unix.Statfs(cache.dir, &stat) {
626                                 maxsize = int64(stat.Bavail) * stat.Bsize * pct / 100
627                                 atomic.StoreInt64(&cache.defaultMaxSize, maxsize)
628                         } else {
629                                 // In this case we will set
630                                 // defaultMaxSize below after
631                                 // measuring current usage.
632                         }
633                 }
634         }
635
636         // Bail if a tidy goroutine is running in a different process.
637         lockfile, err := cache.openFile(filepath.Join(cache.dir, "tmp", "tidy.lock"), os.O_CREATE|os.O_WRONLY)
638         if err != nil {
639                 return
640         }
641         defer lockfile.Close()
642         err = syscall.Flock(int(lockfile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
643         if err != nil {
644                 return
645         }
646
647         type entT struct {
648                 path  string
649                 atime time.Time
650                 size  int64
651         }
652         var ents []entT
653         var totalsize int64
654         filepath.Walk(cache.dir, func(path string, info fs.FileInfo, err error) error {
655                 if err != nil {
656                         cache.debugf("tidy: skipping dir %s: %s", path, err)
657                         return nil
658                 }
659                 if info.IsDir() {
660                         return nil
661                 }
662                 if !strings.HasSuffix(path, cacheFileSuffix) && !strings.HasSuffix(path, tmpFileSuffix) {
663                         return nil
664                 }
665                 var atime time.Time
666                 if stat, ok := info.Sys().(*syscall.Stat_t); ok {
667                         // Access time is available (hopefully the
668                         // filesystem is not mounted with noatime)
669                         atime = time.Unix(stat.Atim.Sec, stat.Atim.Nsec)
670                 } else {
671                         // If access time isn't available we fall back
672                         // to sorting by modification time.
673                         atime = info.ModTime()
674                 }
675                 ents = append(ents, entT{path, atime, info.Size()})
676                 totalsize += info.Size()
677                 return nil
678         })
679         if cache.Logger != nil {
680                 cache.Logger.WithFields(logrus.Fields{
681                         "totalsize": totalsize,
682                         "maxsize":   maxsize,
683                 }).Debugf("DiskCache: checked current cache usage")
684         }
685
686         // If MaxSize wasn't specified and we failed to come up with a
687         // defaultSize above, use the larger of {current cache size, 1
688         // GiB} as the defaultMaxSize for subsequent tidy()
689         // operations.
690         if maxsize == 0 {
691                 if totalsize < 1<<30 {
692                         atomic.StoreInt64(&cache.defaultMaxSize, 1<<30)
693                 } else {
694                         atomic.StoreInt64(&cache.defaultMaxSize, totalsize)
695                 }
696                 cache.debugf("found initial size %d, setting defaultMaxSize %d", totalsize, cache.defaultMaxSize)
697                 return
698         }
699
700         // If we're below MaxSize or there's only one block in the
701         // cache, just update the usage estimate and return.
702         //
703         // (We never delete the last block because that would merely
704         // cause the same block to get re-fetched repeatedly from the
705         // backend.)
706         if totalsize <= maxsize || len(ents) == 1 {
707                 atomic.StoreInt64(&cache.sizeMeasured, totalsize)
708                 atomic.StoreInt64(&cache.sizeEstimated, totalsize)
709                 cache.lastFileCount = int64(len(ents))
710                 return
711         }
712
713         // Set a new size target of maxsize minus 5%.  This makes some
714         // room for sizeEstimate to grow before it triggers another
715         // tidy. We don't want to walk/sort an entire large cache
716         // directory each time we write a block.
717         target := maxsize - (maxsize / 20)
718
719         // Delete oldest entries until totalsize < target or we're
720         // down to a single cached block.
721         sort.Slice(ents, func(i, j int) bool {
722                 return ents[i].atime.Before(ents[j].atime)
723         })
724         deleted := 0
725         for _, ent := range ents {
726                 os.Remove(ent.path)
727                 go cache.deleteHeldopen(ent.path, nil)
728                 deleted++
729                 totalsize -= ent.size
730                 if totalsize <= target || deleted == len(ents)-1 {
731                         break
732                 }
733         }
734
735         if cache.Logger != nil {
736                 cache.Logger.WithFields(logrus.Fields{
737                         "deleted":   deleted,
738                         "totalsize": totalsize,
739                 }).Debugf("DiskCache: remaining cache usage after deleting")
740         }
741         atomic.StoreInt64(&cache.sizeMeasured, totalsize)
742         atomic.StoreInt64(&cache.sizeEstimated, totalsize)
743         cache.lastFileCount = int64(len(ents) - deleted)
744 }