-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpruner.go
131 lines (114 loc) · 2.75 KB
/
pruner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package k6provider
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"sync"
"time"
)
// Pruner prunes binaries suing a LRU policy to enforce a limit
// defined in a high-water-mark.
type Pruner struct {
pruneLock sync.Mutex
dirLock *dirLock
dir string
hwm int64
pruneInterval time.Duration
lastPrune time.Time
}
type pruneTarget struct {
path string
size int64
timestamp time.Time
}
// NewPruner creates a [Pruner] given its high-water-mark limit, and the
// prune interval
func NewPruner(dir string, hwm int64, pruneInterval time.Duration) *Pruner {
return &Pruner{
dirLock: newDirLock(dir),
dir: dir,
hwm: hwm,
pruneInterval: pruneInterval,
}
}
// Touch update access time because reading the file not always updates it
func (p *Pruner) Touch(binPath string) {
if p.hwm > 0 {
p.pruneLock.Lock()
defer p.pruneLock.Unlock()
_ = os.Chtimes(binPath, time.Now(), time.Now())
}
}
// Prune the cache of least recently used files
func (p *Pruner) Prune() error {
if p.hwm == 0 {
return nil
}
// if a lock exists, another prune is in progress
if !p.pruneLock.TryLock() {
return nil
}
defer p.pruneLock.Unlock()
if time.Since(p.lastPrune) < p.pruneInterval {
return nil
}
p.lastPrune = time.Now()
// prevent concurrent prune to the directory
err := p.dirLock.tryLock()
if err != nil {
// is locked, another pruner must be running (maybe another process)
if errors.Is(err, errLocked) {
return nil
}
return fmt.Errorf("%w: %w", ErrPruningCache, err)
}
defer func() {
_ = p.dirLock.unlock()
}()
binaries, err := os.ReadDir(p.dir)
if err != nil {
return fmt.Errorf("%w: %w", ErrPruningCache, err)
}
errs := []error{ErrPruningCache}
cacheSize := int64(0)
pruneTargets := []pruneTarget{}
for _, binDir := range binaries {
// skip any spurious file, each binary is in a directory
if !binDir.IsDir() {
continue
}
binPath := filepath.Join(p.dir, binDir.Name(), k6Binary)
binInfo, err := os.Stat(binPath)
if err != nil {
errs = append(errs, err)
continue
}
cacheSize += binInfo.Size()
pruneTargets = append(
pruneTargets,
pruneTarget{
path: filepath.Dir(binPath), // we are going to prune the directory
size: binInfo.Size(),
timestamp: binInfo.ModTime(),
})
}
if cacheSize <= p.hwm {
return nil
}
sort.Slice(pruneTargets, func(i, j int) bool {
return pruneTargets[i].timestamp.Before(pruneTargets[j].timestamp)
})
for _, target := range pruneTargets {
if err := os.RemoveAll(target.path); err != nil {
errs = append(errs, err)
continue
}
cacheSize -= target.size
if cacheSize <= p.hwm {
return nil
}
}
return fmt.Errorf("%w cache could not be pruned", errors.Join(errs...))
}