mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
fix(cache): give a re-created cache entry its own file
Create re-opened the path with O_TRUNC, which shrinks the file out from under an older stream that may still be serving readers. stream.Reader then hits EOF from the OS at the new, shorter length while the broadcaster still reports the original size, so Wait() reports 'more data exists' and the reader retries forever - a tight pread loop that burns a core and never releases its handle, which in turn blocks Stream.Remove() indefinitely. Unlink first and create with O_EXCL so the new entry gets a fresh inode. Existing readers keep their descriptor on the old inode, see its full contents, and reach a clean EOF. Note this does not address the deferred unlink deleting the re-created file, which is handled separately by the re-fetch in fileCache.Get.
This commit is contained in:
parent
0ce5bd4148
commit
4b9d2178cc
7
utils/cache/spread_fs.go
vendored
7
utils/cache/spread_fs.go
vendored
@ -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) {
|
||||
|
||||
33
utils/cache/spread_fs_test.go
vendored
33
utils/cache/spread_fs_test.go
vendored
@ -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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user