Skip to content

Commit 37709aa

Browse files
committed
Implement an artifact cache in the depot.
We cache up to 500MB of unused artifacts by default, but this size is configurable.
1 parent cc79a08 commit 37709aa

File tree

7 files changed

+142
-5
lines changed

7 files changed

+142
-5
lines changed

internal/constants/constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,3 +493,6 @@ const IgnoreEnvEnvVarName = "ACTIVESTATE_CLI_IGNORE_ENV"
493493

494494
// ProgressUrlPathName is the trailing path for a project's build progress.
495495
const BuildProgressUrlPathName = "distributions"
496+
497+
// RuntimeCacheSizeConfigKey is the config key for the runtime cache size.
498+
const RuntimeCacheSizeConfigKey = "runtime.cache_size"

internal/fileutils/fileutils.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,3 +1299,22 @@ func windowsPathToPosixPath(path string) string {
12991299
func posixPathToWindowsPath(path string) string {
13001300
return strings.ReplaceAll(path, "/", "\\")
13011301
}
1302+
1303+
// GetDirSize returns the on-disk size of the given directory and all of its contents.
1304+
// Does not follow symlinks.
1305+
func GetDirSize(dir string) (int64, error) {
1306+
var size int64
1307+
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
1308+
if err != nil {
1309+
return nil
1310+
}
1311+
if !info.IsDir() {
1312+
size += info.Size()
1313+
}
1314+
return nil
1315+
})
1316+
if err != nil {
1317+
return size, errs.Wrap(err, "Unable to compute size")
1318+
}
1319+
return size, nil
1320+
}

internal/runbits/runtime/runtime.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ func Update(
281281
if proj.IsPortable() {
282282
rtOpts = append(rtOpts, runtime.WithPortable())
283283
}
284+
rtOpts = append(rtOpts, runtime.WithCacheSize(prime.Config().GetInt(constants.RuntimeCacheSizeConfigKey)))
284285

285286
if isArmPlatform(buildPlan) {
286287
prime.Output().Notice(locale.Tl("warning_arm_unstable", "[WARNING]Warning:[/RESET] You are using an ARM64 architecture, which is currently unstable. While it may work, you might encounter issues."))

pkg/runtime/depot.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import (
77
"os"
88
"path/filepath"
99
"runtime"
10+
"sort"
1011
"sync"
12+
"time"
1113

1214
"github.com/go-openapi/strfmt"
1315

16+
"github.com/ActiveState/cli/internal/constants"
1417
"github.com/ActiveState/cli/internal/errs"
1518
"github.com/ActiveState/cli/internal/fileutils"
1619
"github.com/ActiveState/cli/internal/installation/storage"
1720
"github.com/ActiveState/cli/internal/logging"
21+
configMediator "github.com/ActiveState/cli/internal/mediators/config"
22+
"github.com/ActiveState/cli/internal/multilog"
1823
"github.com/ActiveState/cli/internal/sliceutils"
1924
"github.com/ActiveState/cli/internal/smartlink"
2025
)
@@ -24,7 +29,8 @@ const (
2429
)
2530

2631
type depotConfig struct {
27-
Deployments map[strfmt.UUID][]deployment `json:"deployments"`
32+
Deployments map[strfmt.UUID][]deployment `json:"deployments"`
33+
Cache map[strfmt.UUID]*artifactInfo `json:"cache"`
2834
}
2935

3036
type deployment struct {
@@ -41,6 +47,12 @@ const (
4147
deploymentTypeCopy = "copy"
4248
)
4349

50+
type artifactInfo struct {
51+
InUse bool `json:"inUse"`
52+
Size int64 `json:"size"`
53+
LastAccessTime int64 `json:"lastAccessTime"`
54+
}
55+
4456
type ErrVolumeMismatch struct {
4557
DepotVolume string
4658
PathVolume string
@@ -55,8 +67,15 @@ type depot struct {
5567
depotPath string
5668
artifacts map[strfmt.UUID]struct{}
5769
fsMutex *sync.Mutex
70+
cacheSize int64
71+
}
72+
73+
func init() {
74+
configMediator.RegisterOption(constants.RuntimeCacheSizeConfigKey, configMediator.Int, 500)
5875
}
5976

77+
const MB int64 = 1024 * 1024
78+
6079
func newDepot(runtimePath string) (*depot, error) {
6180
depotPath := filepath.Join(storage.CachePath(), depotName)
6281

@@ -73,6 +92,7 @@ func newDepot(runtimePath string) (*depot, error) {
7392
result := &depot{
7493
config: depotConfig{
7594
Deployments: map[strfmt.UUID][]deployment{},
95+
Cache: map[strfmt.UUID]*artifactInfo{},
7696
},
7797
depotPath: depotPath,
7898
artifacts: map[strfmt.UUID]struct{}{},
@@ -122,6 +142,10 @@ func newDepot(runtimePath string) (*depot, error) {
122142
return result, nil
123143
}
124144

145+
func (d *depot) SetCacheSize(mb int) {
146+
d.cacheSize = int64(mb) * MB
147+
}
148+
125149
func (d *depot) Exists(id strfmt.UUID) bool {
126150
_, ok := d.artifacts[id]
127151
return ok
@@ -196,6 +220,7 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
196220
Files: files.RelativePaths(),
197221
RelativeSrc: relativeSrc,
198222
})
223+
d.recordUse(id)
199224

200225
return nil
201226
}
@@ -253,10 +278,28 @@ func (d *depot) DeployViaCopy(id strfmt.UUID, relativeSrc, absoluteDest string)
253278
Files: files.RelativePaths(),
254279
RelativeSrc: relativeSrc,
255280
})
281+
d.recordUse(id)
256282

257283
return nil
258284
}
259285

286+
func (d *depot) recordUse(id strfmt.UUID) {
287+
// Ensure a cache entry for this artifact exists and then update its last access time.
288+
if _, exists := d.config.Cache[id]; !exists {
289+
size, err := fileutils.GetDirSize(d.Path(id))
290+
if err != nil {
291+
multilog.Error("Could not get artifact size on disk: %v", err)
292+
size = 0
293+
}
294+
logging.Debug("Recording artifact '%s' with size %.1f MB", id.String(), float64(size)/float64(MB))
295+
d.config.Cache[id] = &artifactInfo{Size: size}
296+
} else {
297+
logging.Debug("Recording use of artifact '%s'", id.String())
298+
}
299+
d.config.Cache[id].InUse = true
300+
d.config.Cache[id].LastAccessTime = time.Now().Unix()
301+
}
302+
260303
func (d *depot) Undeploy(id strfmt.UUID, relativeSrc, path string) error {
261304
d.fsMutex.Lock()
262305
defer d.fsMutex.Unlock()
@@ -372,14 +415,14 @@ func (d *depot) getSharedFilesToRedeploy(id strfmt.UUID, deploy deployment, path
372415
// Save will write config changes to disk (ie. links between depot artifacts and runtimes that use it).
373416
// It will also delete any stale artifacts which are not used by any runtime.
374417
func (d *depot) Save() error {
375-
// Delete artifacts that are no longer used
418+
// Mark artifacts that are no longer used and remove the old ones.
376419
for id := range d.artifacts {
377420
if deployments, ok := d.config.Deployments[id]; !ok || len(deployments) == 0 {
378-
if err := os.RemoveAll(d.Path(id)); err != nil {
379-
return errs.Wrap(err, "failed to remove stale artifact")
380-
}
421+
d.config.Cache[id].InUse = false
422+
logging.Debug("Artifact '%s' is no longer in use", id.String())
381423
}
382424
}
425+
d.removeStaleArtifacts()
383426

384427
// Write config file changes to disk
385428
configFile := filepath.Join(d.depotPath, depotFile)
@@ -422,3 +465,39 @@ func someFilesExist(filePaths []string, basePath string) bool {
422465
}
423466
return false
424467
}
468+
469+
// removeStaleArtifacts iterates over all unused artifacts in the depot, sorts them by last access
470+
// time, and removes them until the size of cached artifacts is under the limit.
471+
func (d *depot) removeStaleArtifacts() {
472+
type artifact struct {
473+
id strfmt.UUID
474+
info *artifactInfo
475+
}
476+
var totalSize int64
477+
unusedArtifacts := make([]*artifact, 0)
478+
479+
for id, info := range d.config.Cache {
480+
if !info.InUse {
481+
totalSize += info.Size
482+
unusedArtifacts = append(unusedArtifacts, &artifact{id: id, info: info})
483+
}
484+
}
485+
logging.Debug("There are %d unused artifacts totaling %.1f MB in size", len(unusedArtifacts), float64(totalSize)/float64(MB))
486+
487+
sort.Slice(unusedArtifacts, func(i, j int) bool {
488+
return unusedArtifacts[i].info.LastAccessTime < unusedArtifacts[j].info.LastAccessTime
489+
})
490+
491+
for _, artifact := range unusedArtifacts {
492+
if totalSize <= d.cacheSize {
493+
break // done
494+
}
495+
logging.Debug("Removing cached artifact '%s', last accessed on %s", artifact.id.String(), time.Unix(artifact.info.LastAccessTime, 0).Format(time.UnixDate))
496+
if err := os.RemoveAll(d.Path(artifact.id)); err == nil {
497+
totalSize -= artifact.info.Size
498+
} else {
499+
multilog.Error("Could not delete old artifact: %v", err)
500+
}
501+
delete(d.config.Cache, artifact.id)
502+
}
503+
}

pkg/runtime/options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func WithPortable() SetOpt {
2525
return func(opts *Opts) { opts.Portable = true }
2626
}
2727

28+
func WithCacheSize(mb int) SetOpt {
29+
return func(opts *Opts) { opts.CacheSize = mb }
30+
}
31+
2832
func WithArchive(dir string, platformID strfmt.UUID, ext string) SetOpt {
2933
return func(opts *Opts) {
3034
opts.FromArchive = &fromArchive{dir, platformID, ext}

pkg/runtime/setup.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type Opts struct {
4848
BuildlogFilePath string
4949
BuildProgressUrl string
5050
Portable bool
51+
CacheSize int
5152

5253
FromArchive *fromArchive
5354

@@ -93,6 +94,7 @@ type setup struct {
9394
}
9495

9596
func newSetup(path string, bp *buildplan.BuildPlan, env *envdef.Collection, depot *depot, opts *Opts) (*setup, error) {
97+
depot.SetCacheSize(opts.CacheSize)
9698
installedArtifacts := depot.List(path)
9799

98100
var platformID strfmt.UUID

test/integration/runtime_int_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,35 @@ func (suite *RuntimeIntegrationTestSuite) TestIgnoreEnvironmentVars() {
212212
cp.ExpectExitCode(0)
213213
}
214214

215+
func (suite *RuntimeIntegrationTestSuite) TestRuntimeCache() {
216+
suite.OnlyRunForTags(tagsuite.Critical)
217+
ts := e2e.New(suite.T(), false)
218+
defer ts.Close()
219+
220+
ts.PrepareProject("ActiveState-CLI/Empty", "b55d0e63-db48-43c4-8341-e2b7a1cc134c")
221+
222+
cp := ts.Spawn("install", "shared/zlib")
223+
cp.Expect("Downloading")
224+
cp.ExpectExitCode(0)
225+
226+
cp = ts.Spawn("reset", "-n") // should not remove cached shared/zlib artifact
227+
cp.ExpectExitCode(0)
228+
229+
cp = ts.Spawn("install", "shared/zlib")
230+
cp.ExpectExitCode(0)
231+
suite.Assert().NotContains(cp.Snapshot(), "Downloading", "shared/zlib should have been cached")
232+
233+
cp = ts.Spawn("config", "set", constants.RuntimeCacheSizeConfigKey, "0")
234+
cp.ExpectExitCode(0)
235+
236+
cp = ts.Spawn("reset", "-n") // should remove cached shared/zlib artifact
237+
cp.ExpectExitCode(0)
238+
239+
cp = ts.Spawn("install", "shared/zlib")
240+
cp.Expect("Downloading")
241+
cp.ExpectExitCode(0)
242+
}
243+
215244
func TestRuntimeIntegrationTestSuite(t *testing.T) {
216245
suite.Run(t, new(RuntimeIntegrationTestSuite))
217246
}

0 commit comments

Comments
 (0)