Skip to content

Commit 8c6efdf

Browse files
Detect broken symlinks in the cache and fix them (#1994)
Currently, when an external process deletes a file that a symlink is pointing to in the cache, but not the symlink itself, we will never fix that and the cache will be permanently broken. This makes the `AdvertiseCachedFile` sensitive to the possibility of links being broken. Broken links are removed and the advertisement process is retried to establish a functioning cache state again.
1 parent 7edfbb3 commit 8c6efdf

File tree

2 files changed

+58
-16
lines changed

2 files changed

+58
-16
lines changed

pkg/paths/paths.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,29 @@ func AdvertiseCachedFile(src, dst string) error {
4747
if err != nil {
4848
rel = src
4949
}
50-
// Check if the destination already exists.
51-
if _, err := os.Stat(dst); err == nil {
52-
// Since `src` is unadvertised, it is safe to remove it. Ideally we want this to succeeds,
53-
// but we don't want to fail a build just because we couldn't clean up. This will be
54-
// left for background clean up process based on age.
55-
_ = os.Remove(src)
56-
return nil
50+
51+
// Check what exists at dst using Lstat (doesn't follow symlinks).
52+
// This lets us distinguish between "nothing exists" and "broken symlink".
53+
if _, err := os.Lstat(dst); err == nil {
54+
// Something exists at dst. Check if it's a valid symlink by following it.
55+
if _, err := os.Stat(dst); err == nil {
56+
// Valid symlink exists - another process already advertised.
57+
// Clean up src since it's unadvertised and return.
58+
_ = os.Remove(src)
59+
return nil
60+
}
61+
// Broken symlink (Lstat succeeded but Stat failed) - remove it.
62+
if err := os.Remove(dst); err != nil {
63+
return fmt.Errorf("removing broken symlink %s: %w", dst, err)
64+
}
5765
}
66+
5867
// Create the symlink.
5968
if err := os.Symlink(rel, dst); err != nil {
60-
// Ignore already exists errors. We don't even want to do clean up here even when
61-
// the symlink is pointing somewhere else, to avoid relying too much on file system
62-
// remantics/eventual consistency, etc.
6369
if errors.Is(err, os.ErrExist) {
64-
return nil
70+
// Race condition: something appeared between our Lstat check and Symlink.
71+
// Re-run to handle it properly.
72+
return AdvertiseCachedFile(src, dst)
6573
}
6674
return fmt.Errorf("linking (cached) %s to %s: %w", rel, dst, err)
6775
}

pkg/paths/paths_test.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ func TestAdvertiseCachedFile(t *testing.T) {
2323
tmpDir := t.TempDir()
2424
src1 := tmpDir + "/src1.tmp"
2525
src2 := tmpDir + "/src2.tmp"
26+
src3 := tmpDir + "/src3.tmp"
2627
content := "content"
27-
if err := os.WriteFile(src1, []byte(content), 0644); err != nil {
28-
t.Fatal(err)
29-
}
30-
if err := os.WriteFile(src2, []byte(content), 0644); err != nil {
31-
t.Fatal(err)
28+
29+
for _, src := range []string{src1, src2, src3} {
30+
if err := os.WriteFile(src, []byte(content), 0644); err != nil {
31+
t.Fatal(err)
32+
}
3233
}
3334
dst := tmpDir + "/target"
3435
t.Run("dst does not exists", func(t *testing.T) {
@@ -64,4 +65,37 @@ func TestAdvertiseCachedFile(t *testing.T) {
6465
t.Fatalf("src2 should be removed: %v", err)
6566
}
6667
})
68+
69+
t.Run("dst exists, but is broken", func(t *testing.T) {
70+
// check the symlink
71+
rel1, err := filepath.Rel(filepath.Dir(dst), src1)
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
if l, err := os.Readlink(dst); err != nil {
76+
t.Fatal(err)
77+
} else if l != rel1 {
78+
t.Fatalf("unexpected symlink: %s != %s", l, src2)
79+
}
80+
81+
// remove the target to break the symlink
82+
if err := os.Remove(src1); err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
// now advertise src3 to dst
87+
if err := AdvertiseCachedFile(src3, dst); err != nil {
88+
t.Fatal(err)
89+
}
90+
// check the symlink
91+
rel2, err := filepath.Rel(filepath.Dir(dst), src3)
92+
if err != nil {
93+
t.Fatal(err)
94+
}
95+
if l, err := os.Readlink(dst); err != nil {
96+
t.Fatal(err)
97+
} else if l != rel2 {
98+
t.Fatalf("symlink should be updated: %s != %s", l, src2)
99+
}
100+
})
67101
}

0 commit comments

Comments
 (0)