Skip to content

Commit db61eae

Browse files
authored
Gracefully handle cached files being deleted (#1959)
The global cache in the `apk` package stores cached `expandapk.APKExpanded` structs in memory, which references files on disk and contains readers for those files. The in-memory data is currently never invalidated, which can lead to problems if the files on disk are deleted. This PR updates the cache to check if the backing file still exists before returning data from the cache. If it's absent, the APK data is re-populated.
1 parent bb43366 commit db61eae

File tree

2 files changed

+40
-16
lines changed

2 files changed

+40
-16
lines changed

pkg/apk/apk/implementation.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,6 @@ func (a *APK) CalculateWorld(ctx context.Context, allpkgs []*RepositoryPackage)
640640
for i, pkg := range allpkgs {
641641
g.Go(func() error {
642642
expanded, err := a.expandPackage(ctx, pkg)
643-
644643
if err != nil {
645644
return fmt.Errorf("expanding %s: %w", pkg.Name, err)
646645
}
@@ -1262,31 +1261,33 @@ type apkResult struct {
12621261
}
12631262

12641263
type apkCache struct {
1265-
// url -> *sync.Once
1264+
// url -> func() apkResult (from sync.OnceValue(...))
12661265
onces sync.Map
1267-
1268-
// url -> apkResult
1269-
resps sync.Map
12701266
}
12711267

12721268
func (c *apkCache) get(ctx context.Context, a *APK, pkg InstallablePackage) (*expandapk.APKExpanded, error) {
12731269
u := pkg.URL()
1274-
// Do all the expensive things inside the once.
1275-
once, _ := c.onces.LoadOrStore(u, &sync.Once{})
1276-
once.(*sync.Once).Do(func() {
1270+
// Do all the expensive things inside sync.OnceValue()
1271+
fn := func() apkResult {
12771272
exp, err := expandPackage(ctx, a, pkg)
1278-
c.resps.Store(u, apkResult{
1273+
return apkResult{
12791274
exp: exp,
12801275
err: err,
1281-
})
1282-
})
1283-
1284-
v, ok := c.resps.Load(u)
1285-
if !ok {
1286-
panic(fmt.Errorf("did not see apk %q after writing it", u))
1276+
}
1277+
}
1278+
once, cached := c.onces.LoadOrStore(u, sync.OnceValue(fn))
1279+
result := once.(func() apkResult)()
1280+
if cached && result.exp != nil {
1281+
// If we find a value in the cache, we should check to make sure the tar file it references still exists.
1282+
// If it references a non-existent file, we should act as though this was a cache miss and expand the
1283+
// APK again.
1284+
if _, err := os.Stat(result.exp.TarFile); os.IsNotExist(err) {
1285+
newValue := sync.OnceValue(fn)
1286+
c.onces.Store(u, newValue)
1287+
result = newValue()
1288+
}
12871289
}
12881290

1289-
result := v.(apkResult)
12901291
return result.exp, result.err
12911292
}
12921293

pkg/apk/apk/implementation_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,29 @@ func TestFetchPackage(t *testing.T) {
753753
require.NoError(t, err, "unable to read previous apk file")
754754
require.Equal(t, apk1, apk2, "apk files do not match")
755755
})
756+
t.Run("handle missing cache files when expanding APK", func(t *testing.T) {
757+
tmpDir := t.TempDir()
758+
a := prepLayout(t, tmpDir)
759+
// Fill the cache
760+
exp, err := a.expandPackage(ctx, pkg)
761+
require.NoError(t, err, "unable to expand package")
762+
_, err = os.Stat(exp.TarFile)
763+
require.NoError(t, err, "unable to stat cached tar file")
764+
// Delete the tar file from the cache
765+
require.NoError(t, os.Remove(exp.TarFile), "unable to delete cached tar file")
766+
_, err = os.Stat(exp.TarFile)
767+
require.ErrorIs(t, err, os.ErrNotExist, "unexpectedly able to stat cached tar file that should have been deleted")
768+
769+
// Expand the package again, this should re-populate the cache.
770+
exp2, err := a.expandPackage(ctx, pkg)
771+
require.NoError(t, err, "unable to expandPackage after deleting cached tar file")
772+
773+
// We should be able to read the APK contents
774+
rc, err := exp2.APK()
775+
require.NoError(t, err, "unable to get reader for APK()")
776+
_, err = io.ReadAll(rc)
777+
require.NoError(t, err, "unable to read APK contents")
778+
})
756779
t.Run("cache hit no etag", func(t *testing.T) {
757780
tmpDir := t.TempDir()
758781
a := prepLayout(t, tmpDir)

0 commit comments

Comments
 (0)