From d4ff302bdb60fc4cb728b3a902f3d9eecd2f6f15 Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:53:33 -0400 Subject: [PATCH 1/7] perf(segcache): load catalog in background to avoid blocking startup loadCatalog was called synchronously in NewSegmentCache, statting every cached segment on disk before the constructor returned. On a large cache this blocked the entire serve startup path for seconds to minutes. Move the load into Manager.Start as a third wg-tracked goroutine (mirroring the existing cleanupLoop/catalogFlushLoop split). NewSegmentCache now constructs only; LoadCatalog is exported so tests can call it synchronously. The merge loop now skips keys already present (a Put during load already wrote a newer entry and .seg file) instead of clobbering c.items, fixing the race that the old overwrite would have introduced. --- internal/nzbfilesystem/segcache/cache.go | 32 +++++++++++------ internal/nzbfilesystem/segcache/cache_test.go | 36 +++++++++++++++++++ internal/nzbfilesystem/segcache/manager.go | 13 +++++-- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/internal/nzbfilesystem/segcache/cache.go b/internal/nzbfilesystem/segcache/cache.go index 8fa12f479..1096cf357 100644 --- a/internal/nzbfilesystem/segcache/cache.go +++ b/internal/nzbfilesystem/segcache/cache.go @@ -44,20 +44,19 @@ type SegmentCache struct { dirty atomic.Bool } -// NewSegmentCache creates a new segment cache, loading any existing catalog. +// NewSegmentCache creates a new segment cache. It does NOT load any existing +// catalog; call LoadCatalog to hydrate from disk. Manager.Start runs that load +// in a background goroutine so the per-segment stat loop does not block boot. func NewSegmentCache(cfg Config, logger *slog.Logger) (*SegmentCache, error) { if err := os.MkdirAll(cfg.CachePath, 0o755); err != nil { return nil, fmt.Errorf("segcache: create cache dir %s: %w", cfg.CachePath, err) } - c := &SegmentCache{ + return &SegmentCache{ items: make(map[string]*cacheEntry), config: cfg, logger: logger, - } - c.loadCatalog() - - return c, nil + }, nil } // Has reports whether the segment is present in the cache (no disk I/O). @@ -245,8 +244,13 @@ func (c *SegmentCache) SaveCatalog() error { return nil } -func (c *SegmentCache) loadCatalog() { +// LoadCatalog hydrates the in-memory catalog from catalog.json on disk, statting +// each .seg file and dropping entries whose data is missing. Safe to call +// concurrently with Has/Get/Put: loaded entries merge in, and any key already +// present (written while the load was running) is kept as-is. +func (c *SegmentCache) LoadCatalog() { catalogPath := filepath.Join(c.config.CachePath, "catalog.json") + c.logger.Info("segcache: loading catalog", "path", catalogPath) data, err := os.ReadFile(catalogPath) if err != nil { @@ -270,8 +274,16 @@ func (c *SegmentCache) loadCatalog() { } } - c.items = valid - c.totalSize = totalSize + c.mu.Lock() + for id, e := range valid { + if _, exists := c.items[id]; exists { + continue // a Put during load already wrote a newer entry + } + c.items[id] = e + c.totalSize += e.Size + } + loaded, total := len(valid), c.totalSize + c.mu.Unlock() - c.logger.Info("segcache: catalog loaded", "items", len(valid), "total_bytes", totalSize) + c.logger.Info("segcache: catalog loaded", "items", loaded, "total_bytes", total) } diff --git a/internal/nzbfilesystem/segcache/cache_test.go b/internal/nzbfilesystem/segcache/cache_test.go index 27857f198..b8020f522 100644 --- a/internal/nzbfilesystem/segcache/cache_test.go +++ b/internal/nzbfilesystem/segcache/cache_test.go @@ -99,6 +99,7 @@ func TestCacheSaveCatalogAndReload(t *testing.T) { // Load a new cache from the same directory. c2, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) + c2.LoadCatalog() assert.True(t, c2.Has("persist@msg"), "reloaded cache should contain persisted entry") got, ok := c2.Get("persist@msg") @@ -152,7 +153,42 @@ func TestCacheCatalogSurvivesMissingSegFiles(t *testing.T) { // Reload: only "good@msg" should survive. c2, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) + c2.LoadCatalog() assert.True(t, c2.Has("good@msg")) assert.False(t, c2.Has("bad@msg"), "entry with missing seg file should be dropped on reload") } + +// TestCacheLoadCatalogPreservesConcurrentPut guards the merge-under-lock contract +// introduced when loadCatalog was backgrounded. When LoadCatalog runs after a Put +// has already written a newer entry for the same message ID (the race window that +// backgrounding the load created), the stale catalog entry must be skipped, not +// clobber the in-memory map. If this breaks, a segment downloaded during cold load +// silently reverts to stale data on the next Get. +func TestCacheLoadCatalogPreservesConcurrentPut(t *testing.T) { + dir := t.TempDir() + cfg := segcache.Config{ + CachePath: dir, + MaxSizeBytes: 10 * 1024 * 1024, + } + + // Write a stale catalog entry for "race@msg". + c1, err := segcache.NewSegmentCache(cfg, slog.Default()) + require.NoError(t, err) + require.NoError(t, c1.Put("race@msg", []byte("stale"))) + require.NoError(t, c1.SaveCatalog()) + + // New cache — constructor no longer loads. Put newer data before LoadCatalog + // runs, simulating a segment downloaded during cold load. + c2, err := segcache.NewSegmentCache(cfg, slog.Default()) + require.NoError(t, err) + require.NoError(t, c2.Put("race@msg", []byte("fresh"))) + + c2.LoadCatalog() + + // The Put's entry must win; the stale catalog entry must not clobber it. + got, ok := c2.Get("race@msg") + require.True(t, ok) + assert.Equal(t, []byte("fresh"), got) + assert.EqualValues(t, len("fresh"), c2.TotalSize()) +} diff --git a/internal/nzbfilesystem/segcache/manager.go b/internal/nzbfilesystem/segcache/manager.go index 9c5ddb934..b20302241 100644 --- a/internal/nzbfilesystem/segcache/manager.go +++ b/internal/nzbfilesystem/segcache/manager.go @@ -86,9 +86,18 @@ func NewManager(cfg ManagerConfig, logger *slog.Logger) (*Manager, error) { }, nil } -// Start launches background maintenance goroutines. +// Start launches background maintenance goroutines, including the initial +// catalog load (run here, not in NewManager, so boot is not blocked by the +// per-segment stat loop). Stop waits for all of them. func (m *Manager) Start(_ context.Context) { - m.wg.Add(2) + m.wg.Add(3) + // ponytail: load runs here instead of the constructor so the per-segment stat + // loop does not block boot. Stop() waits via wg. Thread ctx into LoadCatalog + // only if reconfig-during-cold-load latency ever matters. + go func() { + defer m.wg.Done() + m.cache.LoadCatalog() + }() go m.cleanupLoop() go m.catalogFlushLoop() } From b3aa69e99b35e32e3d6892ed8eedaf95c7f1001d Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:53:33 -0400 Subject: [PATCH 2/7] fix(segcache): update startup log to reflect async catalog loading The old message "Segment cache initialized" implied the catalog was warm. Now that the load runs in a background goroutine, reword to "Segment cache started (catalog loads in background)". --- cmd/altmount/cmd/setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/altmount/cmd/setup.go b/cmd/altmount/cmd/setup.go index 979b0b5f7..50846f61a 100644 --- a/cmd/altmount/cmd/setup.go +++ b/cmd/altmount/cmd/setup.go @@ -329,7 +329,7 @@ func initializeSegmentCache(ctx context.Context, cfg *config.Config, source *seg mgr.Start(ctx) source.Swap(mgr) - slog.InfoContext(ctx, "Segment cache initialized", + slog.InfoContext(ctx, "Segment cache started (catalog loads in background)", "cache_path", mgrCfg.CachePath, "max_size_bytes", mgrCfg.MaxSizeBytes, "expiry_duration", mgrCfg.ExpiryDuration) From 35bd0e3f29d3348d66adde0bdde3cf1ea7b09274 Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:24:15 -0400 Subject: [PATCH 3/7] While hydrating the cache, skip adding to cache until loaded --- internal/nzbfilesystem/segcache/cache.go | 29 ++++++++-------- .../segcache/cache_internal_test.go | 32 ++++++++++++++++++ internal/nzbfilesystem/segcache/cache_test.go | 33 ------------------- internal/nzbfilesystem/segcache/manager.go | 8 +++-- 4 files changed, 53 insertions(+), 49 deletions(-) create mode 100644 internal/nzbfilesystem/segcache/cache_internal_test.go diff --git a/internal/nzbfilesystem/segcache/cache.go b/internal/nzbfilesystem/segcache/cache.go index 1096cf357..ab6d11e1a 100644 --- a/internal/nzbfilesystem/segcache/cache.go +++ b/internal/nzbfilesystem/segcache/cache.go @@ -42,6 +42,7 @@ type SegmentCache struct { logger *slog.Logger totalSize int64 dirty atomic.Bool + loading atomic.Bool } // NewSegmentCache creates a new segment cache. It does NOT load any existing @@ -95,6 +96,13 @@ func (c *SegmentCache) Get(messageID string) ([]byte, bool) { // Put stores segment bytes atomically (temp-write + rename). func (c *SegmentCache) Put(messageID string, data []byte) error { + // Skip caching while the catalog hydrates. The segment is still served from + // Usenet, just not persisted this round; it gets cached on the next request + // after load. Avoids Put/load lock contention on boot. + if c.loading.Load() { + return nil + } + h := sha256.Sum256([]byte(messageID)) filename := hex.EncodeToString(h[:]) + ".seg" dataPath := filepath.Join(c.config.CachePath, filename) @@ -245,16 +253,17 @@ func (c *SegmentCache) SaveCatalog() error { } // LoadCatalog hydrates the in-memory catalog from catalog.json on disk, statting -// each .seg file and dropping entries whose data is missing. Safe to call -// concurrently with Has/Get/Put: loaded entries merge in, and any key already -// present (written while the load was running) is kept as-is. +// each .seg file and dropping entries whose data is missing. Put is gated off +// (see the loading flag) for the duration, so the load can assign the map +// wholesale without racing a concurrent writer. Clears the gate when done. func (c *SegmentCache) LoadCatalog() { + defer c.loading.Store(false) + catalogPath := filepath.Join(c.config.CachePath, "catalog.json") c.logger.Info("segcache: loading catalog", "path", catalogPath) data, err := os.ReadFile(catalogPath) if err != nil { - // No existing catalog; start fresh. return } @@ -275,15 +284,9 @@ func (c *SegmentCache) LoadCatalog() { } c.mu.Lock() - for id, e := range valid { - if _, exists := c.items[id]; exists { - continue // a Put during load already wrote a newer entry - } - c.items[id] = e - c.totalSize += e.Size - } - loaded, total := len(valid), c.totalSize + c.items = valid + c.totalSize = totalSize c.mu.Unlock() - c.logger.Info("segcache: catalog loaded", "items", loaded, "total_bytes", total) + c.logger.Info("segcache: catalog loaded", "items", len(valid), "total_bytes", totalSize) } diff --git a/internal/nzbfilesystem/segcache/cache_internal_test.go b/internal/nzbfilesystem/segcache/cache_internal_test.go new file mode 100644 index 000000000..9caba23e0 --- /dev/null +++ b/internal/nzbfilesystem/segcache/cache_internal_test.go @@ -0,0 +1,32 @@ +package segcache + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPutIgnoredWhileLoading guards the loading gate that replaced the +// merge-under-lock path: while the catalog hydrates, Put must be a silent no-op +// so a segment downloaded during cold load neither contends with the load nor +// gets clobbered by the wholesale map assignment in LoadCatalog. Once the gate +// clears, Put works normally again. +func TestPutIgnoredWhileLoading(t *testing.T) { + cfg := Config{CachePath: t.TempDir(), MaxSizeBytes: 10 * 1024 * 1024} + c, err := NewSegmentCache(cfg, slog.Default()) + require.NoError(t, err) + + c.loading.Store(true) + + require.NoError(t, c.Put("busy@msg", []byte("data"))) + assert.False(t, c.Has("busy@msg"), "Put must be ignored while catalog is loading") + assert.EqualValues(t, 0, c.ItemCount()) + assert.EqualValues(t, 0, c.TotalSize()) + + c.loading.Store(false) + + require.NoError(t, c.Put("busy@msg", []byte("data"))) + assert.True(t, c.Has("busy@msg")) +} diff --git a/internal/nzbfilesystem/segcache/cache_test.go b/internal/nzbfilesystem/segcache/cache_test.go index b8020f522..a93fedd99 100644 --- a/internal/nzbfilesystem/segcache/cache_test.go +++ b/internal/nzbfilesystem/segcache/cache_test.go @@ -159,36 +159,3 @@ func TestCacheCatalogSurvivesMissingSegFiles(t *testing.T) { assert.False(t, c2.Has("bad@msg"), "entry with missing seg file should be dropped on reload") } -// TestCacheLoadCatalogPreservesConcurrentPut guards the merge-under-lock contract -// introduced when loadCatalog was backgrounded. When LoadCatalog runs after a Put -// has already written a newer entry for the same message ID (the race window that -// backgrounding the load created), the stale catalog entry must be skipped, not -// clobber the in-memory map. If this breaks, a segment downloaded during cold load -// silently reverts to stale data on the next Get. -func TestCacheLoadCatalogPreservesConcurrentPut(t *testing.T) { - dir := t.TempDir() - cfg := segcache.Config{ - CachePath: dir, - MaxSizeBytes: 10 * 1024 * 1024, - } - - // Write a stale catalog entry for "race@msg". - c1, err := segcache.NewSegmentCache(cfg, slog.Default()) - require.NoError(t, err) - require.NoError(t, c1.Put("race@msg", []byte("stale"))) - require.NoError(t, c1.SaveCatalog()) - - // New cache — constructor no longer loads. Put newer data before LoadCatalog - // runs, simulating a segment downloaded during cold load. - c2, err := segcache.NewSegmentCache(cfg, slog.Default()) - require.NoError(t, err) - require.NoError(t, c2.Put("race@msg", []byte("fresh"))) - - c2.LoadCatalog() - - // The Put's entry must win; the stale catalog entry must not clobber it. - got, ok := c2.Get("race@msg") - require.True(t, ok) - assert.Equal(t, []byte("fresh"), got) - assert.EqualValues(t, len("fresh"), c2.TotalSize()) -} diff --git a/internal/nzbfilesystem/segcache/manager.go b/internal/nzbfilesystem/segcache/manager.go index b20302241..6b87dd1d3 100644 --- a/internal/nzbfilesystem/segcache/manager.go +++ b/internal/nzbfilesystem/segcache/manager.go @@ -91,9 +91,11 @@ func NewManager(cfg ManagerConfig, logger *slog.Logger) (*Manager, error) { // per-segment stat loop). Stop waits for all of them. func (m *Manager) Start(_ context.Context) { m.wg.Add(3) - // ponytail: load runs here instead of the constructor so the per-segment stat - // loop does not block boot. Stop() waits via wg. Thread ctx into LoadCatalog - // only if reconfig-during-cold-load latency ever matters. + // Gate Puts here on the caller's goroutine, before source.Swap wires the cache + // live, so a segment downloaded during cold load is skipped rather than + // clobbered by the wholesale map assignment. Load itself runs async so the + // per-segment stat loop does not block boot; Stop() waits via wg. + m.cache.loading.Store(true) go func() { defer m.wg.Done() m.cache.LoadCatalog() From a68dfe7f18f40a59c79ebc03eb2b848be56c138b Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:25:55 -0400 Subject: [PATCH 4/7] readd comment --- internal/nzbfilesystem/segcache/cache.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/nzbfilesystem/segcache/cache.go b/internal/nzbfilesystem/segcache/cache.go index ab6d11e1a..85d622603 100644 --- a/internal/nzbfilesystem/segcache/cache.go +++ b/internal/nzbfilesystem/segcache/cache.go @@ -264,6 +264,7 @@ func (c *SegmentCache) LoadCatalog() { data, err := os.ReadFile(catalogPath) if err != nil { + // No existing catalog; start fresh. return } From 65cd5541351b5ed964a732f49d66465e1e2ce7c7 Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:05:08 -0400 Subject: [PATCH 5/7] Keep within cache --- internal/nzbfilesystem/segcache/cache.go | 9 +++++++-- internal/nzbfilesystem/segcache/manager.go | 5 ----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/nzbfilesystem/segcache/cache.go b/internal/nzbfilesystem/segcache/cache.go index 85d622603..4957f6bf5 100644 --- a/internal/nzbfilesystem/segcache/cache.go +++ b/internal/nzbfilesystem/segcache/cache.go @@ -53,11 +53,16 @@ func NewSegmentCache(cfg Config, logger *slog.Logger) (*SegmentCache, error) { return nil, fmt.Errorf("segcache: create cache dir %s: %w", cfg.CachePath, err) } - return &SegmentCache{ + c := &SegmentCache{ items: make(map[string]*cacheEntry), config: cfg, logger: logger, - }, nil + } + // Gate Puts until LoadCatalog runs and clears this. Set at construction (not + // in Start's goroutine) so a segment downloaded before the async load runs is + // skipped rather than clobbered by the wholesale map assignment. + c.loading.Store(true) + return c, nil } // Has reports whether the segment is present in the cache (no disk I/O). diff --git a/internal/nzbfilesystem/segcache/manager.go b/internal/nzbfilesystem/segcache/manager.go index 6b87dd1d3..967a72b60 100644 --- a/internal/nzbfilesystem/segcache/manager.go +++ b/internal/nzbfilesystem/segcache/manager.go @@ -91,11 +91,6 @@ func NewManager(cfg ManagerConfig, logger *slog.Logger) (*Manager, error) { // per-segment stat loop). Stop waits for all of them. func (m *Manager) Start(_ context.Context) { m.wg.Add(3) - // Gate Puts here on the caller's goroutine, before source.Swap wires the cache - // live, so a segment downloaded during cold load is skipped rather than - // clobbered by the wholesale map assignment. Load itself runs async so the - // per-segment stat loop does not block boot; Stop() waits via wg. - m.cache.loading.Store(true) go func() { defer m.wg.Done() m.cache.LoadCatalog() From 92f1dbb97d18e2e2b0353a7700faaead323a85bc Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:47:29 -0400 Subject: [PATCH 6/7] Fix tests --- internal/nzbfilesystem/segcache/cache_test.go | 5 +++++ internal/nzbfilesystem/segcache/file_test.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/internal/nzbfilesystem/segcache/cache_test.go b/internal/nzbfilesystem/segcache/cache_test.go index a93fedd99..296ea5740 100644 --- a/internal/nzbfilesystem/segcache/cache_test.go +++ b/internal/nzbfilesystem/segcache/cache_test.go @@ -22,6 +22,9 @@ func newTestCache(t *testing.T, maxBytes int64, expiry time.Duration) *segcache. } c, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) + // Clear the loading gate (set in the constructor) so Put is not a no-op. + // Mirrors Manager.Start, which runs LoadCatalog before serving Puts. + c.LoadCatalog() return c } @@ -93,6 +96,7 @@ func TestCacheSaveCatalogAndReload(t *testing.T) { // Write entries, save catalog, then reload. c1, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) + c1.LoadCatalog() require.NoError(t, c1.Put("persist@msg", []byte("persistent data"))) require.NoError(t, c1.SaveCatalog()) @@ -130,6 +134,7 @@ func TestCacheCatalogSurvivesMissingSegFiles(t *testing.T) { c1, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) + c1.LoadCatalog() require.NoError(t, c1.Put("good@msg", []byte("good"))) require.NoError(t, c1.Put("bad@msg", []byte("will be deleted"))) require.NoError(t, c1.SaveCatalog()) diff --git a/internal/nzbfilesystem/segcache/file_test.go b/internal/nzbfilesystem/segcache/file_test.go index 53bc4ee5e..7996897dc 100644 --- a/internal/nzbfilesystem/segcache/file_test.go +++ b/internal/nzbfilesystem/segcache/file_test.go @@ -19,6 +19,9 @@ func newDefaultTestCache(t *testing.T) *segcache.SegmentCache { } cache, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) + // Clear the loading gate (set in the constructor) so Put is not a no-op. + // Mirrors Manager.Start, which runs LoadCatalog before serving Puts. + cache.LoadCatalog() return cache } From 35adb55ebbbe0b6df7037658b52d2d4096280e7d Mon Sep 17 00:00:00 2001 From: evulhotdog <365456+evulhotdog@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:38:47 -0400 Subject: [PATCH 7/7] refactor(segcache): move loading gate into LoadCatalog Per review: the loading flag's lifecycle (set on entry, cleared on exit) now lives entirely in LoadCatalog. NewSegmentCache no longer pokes at load-state, so a cache can no longer be left permanently gating Puts if a future caller forgets to call LoadCatalog. --- internal/nzbfilesystem/segcache/cache.go | 8 +++----- internal/nzbfilesystem/segcache/cache_test.go | 3 +-- internal/nzbfilesystem/segcache/file_test.go | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/internal/nzbfilesystem/segcache/cache.go b/internal/nzbfilesystem/segcache/cache.go index 4957f6bf5..e53d89d8d 100644 --- a/internal/nzbfilesystem/segcache/cache.go +++ b/internal/nzbfilesystem/segcache/cache.go @@ -58,10 +58,6 @@ func NewSegmentCache(cfg Config, logger *slog.Logger) (*SegmentCache, error) { config: cfg, logger: logger, } - // Gate Puts until LoadCatalog runs and clears this. Set at construction (not - // in Start's goroutine) so a segment downloaded before the async load runs is - // skipped rather than clobbered by the wholesale map assignment. - c.loading.Store(true) return c, nil } @@ -260,8 +256,10 @@ func (c *SegmentCache) SaveCatalog() error { // LoadCatalog hydrates the in-memory catalog from catalog.json on disk, statting // each .seg file and dropping entries whose data is missing. Put is gated off // (see the loading flag) for the duration, so the load can assign the map -// wholesale without racing a concurrent writer. Clears the gate when done. +// wholesale without racing a concurrent writer. Sets the gate on entry and +// clears it on exit. func (c *SegmentCache) LoadCatalog() { + c.loading.Store(true) defer c.loading.Store(false) catalogPath := filepath.Join(c.config.CachePath, "catalog.json") diff --git a/internal/nzbfilesystem/segcache/cache_test.go b/internal/nzbfilesystem/segcache/cache_test.go index 296ea5740..3cea87878 100644 --- a/internal/nzbfilesystem/segcache/cache_test.go +++ b/internal/nzbfilesystem/segcache/cache_test.go @@ -22,8 +22,7 @@ func newTestCache(t *testing.T, maxBytes int64, expiry time.Duration) *segcache. } c, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) - // Clear the loading gate (set in the constructor) so Put is not a no-op. - // Mirrors Manager.Start, which runs LoadCatalog before serving Puts. + // Hydrate + clear the loading gate; mirrors Manager.Start before serving Puts. c.LoadCatalog() return c } diff --git a/internal/nzbfilesystem/segcache/file_test.go b/internal/nzbfilesystem/segcache/file_test.go index 7996897dc..2daf63938 100644 --- a/internal/nzbfilesystem/segcache/file_test.go +++ b/internal/nzbfilesystem/segcache/file_test.go @@ -19,8 +19,7 @@ func newDefaultTestCache(t *testing.T) *segcache.SegmentCache { } cache, err := segcache.NewSegmentCache(cfg, slog.Default()) require.NoError(t, err) - // Clear the loading gate (set in the constructor) so Put is not a no-op. - // Mirrors Manager.Start, which runs LoadCatalog before serving Puts. + // Hydrate + clear the loading gate; mirrors Manager.Start before serving Puts. cache.LoadCatalog() return cache }