diff --git a/utils/cache/spread_fs.go b/utils/cache/spread_fs.go index 647439790..131f8a708 100644 --- a/utils/cache/spread_fs.go +++ b/utils/cache/spread_fs.go @@ -128,7 +128,12 @@ func (sfs *spreadFS) Create(name string) (stream.File, error) { if err != nil { return nil, err } - return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + // Unlink instead of truncating: an older stream may still be serving readers from + // this path, and shrinking the file under them spins them at a premature EOF. + if err := os.Remove(name); err != nil && !os.IsNotExist(err) { + return nil, err + } + return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) } func (sfs *spreadFS) Open(name string) (stream.File, error) { diff --git a/utils/cache/spread_fs_test.go b/utils/cache/spread_fs_test.go index 0f88d3a58..ae2d86f05 100644 --- a/utils/cache/spread_fs_test.go +++ b/utils/cache/spread_fs_test.go @@ -1,10 +1,12 @@ package cache import ( + "io" "os" "path/filepath" "strings" + "github.com/djherbis/stream" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -39,6 +41,37 @@ var _ = Describe("Spread FS", func() { }) }) + Describe("Create", func() { + It("leaves an already-open reader's bytes intact", func() { + // A re-created entry must not shrink the file an older stream is still + // serving: its reader would spin forever at the premature EOF. + name := filepath.Join(rootDir, "aa", "bb", "data") + s, err := stream.NewStream(name, fs) + Expect(err).To(BeNil()) + _, err = s.Write([]byte("PARTIAL")) + Expect(err).To(BeNil()) + r, err := s.NextReader() + Expect(err).To(BeNil()) + Expect(s.Close()).To(Succeed()) + + f, err := fs.Create(name) + Expect(err).To(BeNil()) + _, err = f.Write([]byte("GOOD")) + Expect(err).To(BeNil()) + Expect(f.Close()).To(Succeed()) + + done := make(chan []byte, 1) + go func() { + b, _ := io.ReadAll(r) + done <- b + }() + Eventually(done).Should(Receive(Equal([]byte("PARTIAL")))) + Expect(r.Close()).To(Succeed()) + + Expect(os.ReadFile(name)).To(Equal([]byte("GOOD"))) + }) + }) + Describe("MarkComplete / Remove markers", func() { It("creates a .complete marker for a data file", func() { data := fs.KeyMapper("song1")