Skip to content

Commit 63726c8

Browse files
snajpae2b-bot[bot]
authored andcommitted
fix(orchestrator): clean up newly created cache files on errors
GitOrigin-RevId: d66d85bebb19857ac5e1979c2a286c53991f50ff
1 parent c2c5e9e commit 63726c8

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

‎packages/orchestrator/pkg/sandbox/block/cache.go‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,26 @@ type Cache struct {
5959
closed atomic.Bool
6060
}
6161

62-
func NewCache(size, blockSize int64, filePath string, dirtyFile bool) (*Cache, error) {
63-
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0o644)
62+
func NewCache(size, blockSize int64, filePath string, dirtyFile bool) (_ *Cache, retErr error) {
63+
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
64+
created := err == nil
65+
if errors.Is(err, os.ErrExist) {
66+
// Existing paths may hold a completed diff. Ownership transfers only
67+
// on success, so a failed constructor must leave them to their caller.
68+
f, err = os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0o644)
69+
}
6470
if err != nil {
6571
return nil, fmt.Errorf("error opening file: %w", err)
6672
}
6773

68-
defer f.Close()
74+
defer func() {
75+
_ = f.Close()
76+
if retErr != nil && created {
77+
if err := os.Remove(filePath); err != nil && !errors.Is(err, os.ErrNotExist) {
78+
retErr = errors.Join(retErr, fmt.Errorf("remove failed cache file: %w", err))
79+
}
80+
}
81+
}()
6982

7083
if size == 0 {
7184
return &Cache{
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//go:build linux
2+
3+
package block
4+
5+
import (
6+
"bytes"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"golang.org/x/sys/unix"
14+
)
15+
16+
func TestCacheConstructionFailureRemovesNewOutput(t *testing.T) {
17+
t.Parallel()
18+
path := filepath.Join(t.TempDir(), "new-output")
19+
cache, err := NewCache(-1, int64(os.Getpagesize()), path, false)
20+
require.Nil(t, cache)
21+
require.ErrorIs(t, err, unix.EINVAL, "the real truncate failure must remain visible")
22+
_, statErr := os.Stat(path)
23+
require.ErrorIs(t, statErr, os.ErrNotExist, "failed construction must remove its newly created output")
24+
}
25+
26+
func TestCacheConstructionFailurePreservesExistingOutput(t *testing.T) {
27+
t.Parallel()
28+
for _, mode := range []string{"regular", "symlink", "dangling-symlink"} {
29+
t.Run(mode, func(t *testing.T) {
30+
t.Parallel()
31+
dir := t.TempDir()
32+
target := filepath.Join(dir, "caller-owned")
33+
path := target
34+
data := []byte("already written diff")
35+
if mode == "dangling-symlink" {
36+
data = nil
37+
} else {
38+
require.NoError(t, os.WriteFile(target, data, 0o600))
39+
}
40+
if mode != "regular" {
41+
path = filepath.Join(dir, "existing-link")
42+
require.NoError(t, os.Symlink(target, path))
43+
}
44+
before, err := os.Lstat(path)
45+
require.NoError(t, err)
46+
cache, err := NewCache(-1, int64(os.Getpagesize()), path, false)
47+
require.Nil(t, cache)
48+
require.ErrorIs(t, err, unix.EINVAL)
49+
after, err := os.Lstat(path)
50+
require.NoError(t, err, "failed construction must not remove a pre-existing caller-owned path")
51+
require.True(t, os.SameFile(before, after))
52+
actual, err := os.ReadFile(target)
53+
require.NoError(t, err)
54+
assert.Equal(t, string(data), string(actual))
55+
})
56+
}
57+
}
58+
59+
func TestCacheConstructionExistingOutputTransfersOnSuccess(t *testing.T) {
60+
t.Parallel()
61+
path := filepath.Join(t.TempDir(), "written-diff")
62+
data := bytes.Repeat([]byte{0x6b}, os.Getpagesize())
63+
require.NoError(t, os.WriteFile(path, data, 0o600))
64+
cache, err := NewCache(int64(len(data)), int64(os.Getpagesize()), path, true)
65+
require.NoError(t, err)
66+
t.Cleanup(func() { _ = cache.Close() })
67+
actual := make([]byte, len(data))
68+
n, err := cache.ReadAt(actual, 0)
69+
require.NoError(t, err)
70+
require.Equal(t, len(data), n)
71+
require.Equal(t, data, actual)
72+
require.NoError(t, cache.Close())
73+
_, err = os.Stat(path)
74+
require.ErrorIs(t, err, os.ErrNotExist, "successful constructor transfers ordinary Close ownership")
75+
}

0 commit comments

Comments
 (0)