Skip to content

Commit 4f87e5f

Browse files
authored
Merge pull request ActiveState#3660 from ActiveState/mitchell/dx-3213
Implement an artifact cache in the depot.
2 parents a6e1485 + 0dfc771 commit 4f87e5f

File tree

7 files changed

+155
-5
lines changed

7 files changed

+155
-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: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ 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"
1822
"github.com/ActiveState/cli/internal/sliceutils"
1923
"github.com/ActiveState/cli/internal/smartlink"
2024
)
@@ -24,7 +28,8 @@ const (
2428
)
2529

2630
type depotConfig struct {
27-
Deployments map[strfmt.UUID][]deployment `json:"deployments"`
31+
Deployments map[strfmt.UUID][]deployment `json:"deployments"`
32+
Cache map[strfmt.UUID]*artifactInfo `json:"cache"`
2833
}
2934

3035
type deployment struct {
@@ -41,6 +46,14 @@ const (
4146
deploymentTypeCopy = "copy"
4247
)
4348

49+
type artifactInfo struct {
50+
InUse bool `json:"inUse"`
51+
Size int64 `json:"size"`
52+
LastAccessTime int64 `json:"lastAccessTime"`
53+
54+
id strfmt.UUID // for convenience when removing stale artifacts; should NOT have json tag
55+
}
56+
4457
type ErrVolumeMismatch struct {
4558
DepotVolume string
4659
PathVolume string
@@ -55,8 +68,15 @@ type depot struct {
5568
depotPath string
5669
artifacts map[strfmt.UUID]struct{}
5770
fsMutex *sync.Mutex
71+
cacheSize int64
72+
}
73+
74+
func init() {
75+
configMediator.RegisterOption(constants.RuntimeCacheSizeConfigKey, configMediator.Int, 500)
5876
}
5977

78+
const MB int64 = 1024 * 1024
79+
6080
func newDepot(runtimePath string) (*depot, error) {
6181
depotPath := filepath.Join(storage.CachePath(), depotName)
6282

@@ -73,6 +93,7 @@ func newDepot(runtimePath string) (*depot, error) {
7393
result := &depot{
7494
config: depotConfig{
7595
Deployments: map[strfmt.UUID][]deployment{},
96+
Cache: map[strfmt.UUID]*artifactInfo{},
7697
},
7798
depotPath: depotPath,
7899
artifacts: map[strfmt.UUID]struct{}{},
@@ -122,6 +143,10 @@ func newDepot(runtimePath string) (*depot, error) {
122143
return result, nil
123144
}
124145

146+
func (d *depot) SetCacheSize(mb int) {
147+
d.cacheSize = int64(mb) * MB
148+
}
149+
125150
func (d *depot) Exists(id strfmt.UUID) bool {
126151
_, ok := d.artifacts[id]
127152
return ok
@@ -196,6 +221,10 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
196221
Files: files.RelativePaths(),
197222
RelativeSrc: relativeSrc,
198223
})
224+
err = d.recordUse(id)
225+
if err != nil {
226+
return errs.Wrap(err, "Could not record artifact use")
227+
}
199228

200229
return nil
201230
}
@@ -253,7 +282,25 @@ func (d *depot) DeployViaCopy(id strfmt.UUID, relativeSrc, absoluteDest string)
253282
Files: files.RelativePaths(),
254283
RelativeSrc: relativeSrc,
255284
})
285+
err = d.recordUse(id)
286+
if err != nil {
287+
return errs.Wrap(err, "Could not record artifact use")
288+
}
289+
290+
return nil
291+
}
256292

293+
func (d *depot) recordUse(id strfmt.UUID) error {
294+
// Ensure a cache entry for this artifact exists and then update its last access time.
295+
if _, exists := d.config.Cache[id]; !exists {
296+
size, err := fileutils.GetDirSize(d.Path(id))
297+
if err != nil {
298+
return errs.Wrap(err, "Could not get artifact size on disk")
299+
}
300+
d.config.Cache[id] = &artifactInfo{Size: size, id: id}
301+
}
302+
d.config.Cache[id].InUse = true
303+
d.config.Cache[id].LastAccessTime = time.Now().Unix()
257304
return nil
258305
}
259306

@@ -372,14 +419,17 @@ func (d *depot) getSharedFilesToRedeploy(id strfmt.UUID, deploy deployment, path
372419
// Save will write config changes to disk (ie. links between depot artifacts and runtimes that use it).
373420
// It will also delete any stale artifacts which are not used by any runtime.
374421
func (d *depot) Save() error {
375-
// Delete artifacts that are no longer used
422+
// Mark artifacts that are no longer used and remove the old ones.
376423
for id := range d.artifacts {
377424
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-
}
425+
d.config.Cache[id].InUse = false
426+
logging.Debug("Artifact '%s' is no longer in use", id.String())
381427
}
382428
}
429+
err := d.removeStaleArtifacts()
430+
if err != nil {
431+
return errs.Wrap(err, "Could not remove stale artifacts")
432+
}
383433

384434
// Write config file changes to disk
385435
configFile := filepath.Join(d.depotPath, depotFile)
@@ -422,3 +472,40 @@ func someFilesExist(filePaths []string, basePath string) bool {
422472
}
423473
return false
424474
}
475+
476+
// removeStaleArtifacts iterates over all unused artifacts in the depot, sorts them by last access
477+
// time, and removes them until the size of cached artifacts is under the limit.
478+
func (d *depot) removeStaleArtifacts() error {
479+
var totalSize int64
480+
unusedArtifacts := make([]*artifactInfo, 0)
481+
482+
for _, info := range d.config.Cache {
483+
if !info.InUse {
484+
totalSize += info.Size
485+
unusedArtifacts = append(unusedArtifacts, info)
486+
}
487+
}
488+
logging.Debug("There are %d unused artifacts totaling %.1f MB in size", len(unusedArtifacts), float64(totalSize)/float64(MB))
489+
490+
sort.Slice(unusedArtifacts, func(i, j int) bool {
491+
return unusedArtifacts[i].LastAccessTime < unusedArtifacts[j].LastAccessTime
492+
})
493+
494+
var rerr error
495+
for _, artifact := range unusedArtifacts {
496+
if totalSize <= d.cacheSize {
497+
break // done
498+
}
499+
if err := os.RemoveAll(d.Path(artifact.id)); err == nil {
500+
totalSize -= artifact.Size
501+
} else {
502+
if err := errs.Wrap(err, "Could not delete old artifact"); rerr == nil {
503+
rerr = err
504+
} else {
505+
rerr = errs.Pack(rerr, err)
506+
}
507+
}
508+
delete(d.config.Cache, artifact.id)
509+
}
510+
return rerr
511+
}

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/ActiveState/cli/internal/constants"
10+
"github.com/ActiveState/cli/internal/fileutils"
1011
"github.com/ActiveState/cli/internal/osutils"
1112
"github.com/ActiveState/cli/internal/testhelpers/e2e"
1213
"github.com/ActiveState/cli/internal/testhelpers/osutil"
@@ -212,6 +213,39 @@ func (suite *RuntimeIntegrationTestSuite) TestIgnoreEnvironmentVars() {
212213
cp.ExpectExitCode(0)
213214
}
214215

216+
func (suite *RuntimeIntegrationTestSuite) TestRuntimeCache() {
217+
suite.OnlyRunForTags(tagsuite.Critical)
218+
ts := e2e.New(suite.T(), false)
219+
defer ts.Close()
220+
221+
ts.PrepareEmptyProject()
222+
223+
cp := ts.Spawn("install", "shared/zlib")
224+
cp.Expect("Downloading")
225+
cp.ExpectExitCode(0)
226+
227+
depot := filepath.Join(ts.Dirs.Cache, "depot")
228+
artifacts, err := fileutils.ListDirSimple(depot, true)
229+
suite.Require().NoError(err)
230+
231+
cp = ts.Spawn("switch", "mingw") // should not remove cached shared/zlib artifact
232+
cp.ExpectExitCode(0)
233+
234+
artifacts2, err := fileutils.ListDirSimple(depot, true)
235+
suite.Require().NoError(err)
236+
suite.Assert().Equal(artifacts, artifacts2, "shared/zlib should have remained in the cache")
237+
238+
cp = ts.Spawn("config", "set", constants.RuntimeCacheSizeConfigKey, "0")
239+
cp.ExpectExitCode(0)
240+
241+
cp = ts.Spawn("switch", "main") // should remove cached shared/zlib artifact
242+
cp.ExpectExitCode(0)
243+
244+
artifacts3, err := fileutils.ListDirSimple(depot, true)
245+
suite.Require().NoError(err)
246+
suite.Assert().NotEqual(artifacts, artifacts3, "shared/zlib should have been removed from the cache")
247+
}
248+
215249
func TestRuntimeIntegrationTestSuite(t *testing.T) {
216250
suite.Run(t, new(RuntimeIntegrationTestSuite))
217251
}

0 commit comments

Comments
 (0)