From 9990b312c711a6d6681f6d6c125ffaaf259eded3 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Sat, 5 Sep 2026 07:00:54 +0000 Subject: [PATCH] fix(positions): use deterministic temp file to prevent dentry slab growth Replaces renameio.WriteFile with a custom atomicWriteFile that uses a deterministic temp path (.tmp) instead of a unique random name on every call. This prevents unbounded kernel dentry slab growth under cgroup v2 when positions are written every 10 seconds. --- .../internal/positions/positions_test.go | 26 +++++++++++ .../positions/write_positions_unix.go | 45 +++++++++++++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/internal/component/loki/source/internal/positions/positions_test.go b/internal/component/loki/source/internal/positions/positions_test.go index cff6f52f811..5dd9b40911a 100644 --- a/internal/component/loki/source/internal/positions/positions_test.go +++ b/internal/component/loki/source/internal/positions/positions_test.go @@ -423,6 +423,32 @@ positions: }, out) } +// TestWritePositionFile_ReusesDentry verifies that writePositionFile uses a +// deterministic temp path (.tmp) rather than a unique random name on +// every call, so the kernel dentry slab does not grow unboundedly under +// cgroup v2. +func TestWritePositionFile_ReusesDentry(t *testing.T) { + tmpDir := t.TempDir() + target := filepath.Join(tmpDir, "positions.yaml") + + // Write multiple times and verify the temp file path stays the same. + for i := 0; i < 3; i++ { + err := writePositionFile(target, map[Entry]string{ + {Path: "/tmp/test.log", Labels: ""}: "100", + }) + require.NoError(t, err) + + // The temp file should either not exist (already renamed) or be the + // deterministic .tmp path. + entries, readErr := os.ReadDir(tmpDir) + require.NoError(t, readErr) + for _, entry := range entries { + require.NotContains(t, entry.Name(), "positions.yaml.", + "unexpected uniquely-named temp file (renameio-style)") + } + } +} + func TestReadEmptyLabels(t *testing.T) { temp := tempFilename(t) defer func() { diff --git a/internal/component/loki/source/internal/positions/write_positions_unix.go b/internal/component/loki/source/internal/positions/write_positions_unix.go index b2490b5cc99..501d96fd2df 100644 --- a/internal/component/loki/source/internal/positions/write_positions_unix.go +++ b/internal/component/loki/source/internal/positions/write_positions_unix.go @@ -10,12 +10,51 @@ import ( "os" "path/filepath" - renameio "github.com/google/renameio/v2" yaml "gopkg.in/yaml.v2" ) const positionFileMode = 0600 +// atomicWriteFile writes buf to filename using a deterministic temp file +// (.tmp), fsyncs, renames over the target, then fsyncs the directory. +// This reuses the same dentry on every write instead of allocating a new one +// (as renameio.WriteFile does), which prevents unbounded kernel dentry slab +// growth under cgroup v2. +func atomicWriteFile(filename string, buf []byte) error { + target := filepath.Clean(filename) + tmp := target + ".tmp" + + f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(positionFileMode)) + if err != nil { + return err + } + + if _, err := f.Write(buf); err != nil { + f.Close() + return err + } + + if err := f.Sync(); err != nil { + f.Close() + return err + } + + if err := f.Close(); err != nil { + return err + } + + if err := os.Rename(tmp, target); err != nil { + return err + } + + dir, err := os.Open(filepath.Dir(target)) + if err != nil { + return err + } + defer dir.Close() + return dir.Sync() +} + func writePositionFile(filename string, positions map[Entry]string) error { buf, err := yaml.Marshal(File{ Positions: positions, @@ -24,7 +63,5 @@ func writePositionFile(filename string, positions map[Entry]string) error { return err } - target := filepath.Clean(filename) - - return renameio.WriteFile(target, buf, os.FileMode(positionFileMode)) + return atomicWriteFile(filename, buf) }