Skip to content

simplefs: Init() does not restore the key index, so the cache is cold after every restart and the directory leaks #60

Description

@v-angolenko

Description

simplefs never restores its key index on startup, so a cache directory populated by a previous process is unreachable after a restart — while the files stay on disk forever. Two consequences: the cache is always cold after a restart, and the directory grows without bound across restarts.

Init() walks the directory, but only to sum actualSize:

files, _ := os.ReadDir(provider.path)
provider.logger.Debugf("Regenerating simplefs cache from files in the given directory.")

for _, f := range files {
	if !f.IsDir() {
		info, _ := f.Info()
		provider.actualSize += info.Size()
	}
}

No cache.Set is issued for any of those files, so the ttlcache index starts empty. Since Get resolves a key to a path through that index, every pre-existing file is orphaned: unreadable, undeletable (eviction is driven by the same index), and permanently counted in actualSize. The debug line "Regenerating simplefs cache from files in the given directory" describes an intent the code does not implement.

This interacts badly with #57: on restart actualSize is initialised from orphaned files that can never be evicted, so a bounded (directory_size) instance starts closer to — or already past — its ceiling, and reaches the deadlock sooner.

Reproduction

simplefs/restart_test.go, against main (v0.0.19):

package simplefs_test

import (
	"crypto/rand"
	"net/http"
	"os"
	"testing"
	"time"

	"github.com/darkweak/storages/core"
	"github.com/darkweak/storages/simplefs"
	"go.uber.org/zap"
)

func newProvider(t *testing.T, dir string) core.Storer {
	t.Helper()
	logger, _ := zap.NewDevelopment()
	p, err := simplefs.Factory(core.CacheProvider{Path: dir}, logger.Sugar(), 0)
	if err != nil {
		t.Fatal(err)
	}
	if err := p.Init(); err != nil {
		t.Fatal(err)
	}
	return p
}

func TestSurvivesRestart(t *testing.T) {
	dir := t.TempDir()

	body := make([]byte, 2048)
	_, _ = rand.Read(body)

	p1 := newProvider(t, dir)
	if err := p1.SetMultiLevel("base", "varied-1", body, http.Header{}, "etag", time.Hour, "real"); err != nil {
		t.Fatal(err)
	}
	if len(p1.Get("varied-1")) == 0 {
		t.Fatal("pre-restart Get returned nothing")
	}

	// Same directory, fresh provider == process restart.
	p2 := newProvider(t, dir)
	files, _ := os.ReadDir(dir)
	t.Logf("after restart: %d files on disk, ListKeys()=%v, Get len=%d", len(files), p2.ListKeys(), len(p2.Get("varied-1")))

	if len(p2.Get("varied-1")) == 0 {
		t.Errorf("cold after restart: %d files remain on disk but are unreachable", len(files))
	}
}

Output:

before restart: 1 files on disk, key readable
WARN  simplefs/simplefs.go:166  Impossible to get the key varied-1 in Simplefs
after restart: 1 files still on disk, ListKeys()=[], Get len=0
--- FAIL: TestSurvivesRestart

Impact

For a filesystem-backed store, surviving a restart is close to the whole point of choosing it over an in-memory one — otherwise the disk write buys nothing over otter, at the cost of unbounded growth. On a long-lived cache (CDN edge, media delivery) every process restart currently leaks the entire working set.

Possible directions

The mapping entries already carry everything needed (variedKey, varied headers, etag, freshness/staleness deadlines — see core.MappingUpdater), so a rebuild is mostly a matter of persisting them alongside the bodies:

  1. Write a sidecar (or a single index file) holding key → path plus the mapping metadata, and repopulate the ttlcache in Init(), dropping entries whose deadline has already passed and deleting their files.
  2. Failing that, Init() should at least delete the files it cannot index, so the directory does not grow without bound and actualSize reflects reality.

Happy to put a PR together for either shape if you have a preference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions