Make cache safe and bounded (thread-safe + eviction + cleanup) #12
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The in-memory cache used in
internal/mailcloak/policy.go(see aroundpolicy.go:14) is currently not concurrency-safe and appears unbounded.This can lead to:
Data races under concurrent policy requests (Postfix policy daemon is typically concurrent).
Unbounded memory growth over time (potentially a slow memory leak pattern).
Stale entries remaining forever if there is no TTL / cleanup.
Goal
Make the cache:
Concurrency-safe (no races).
Bounded (max size + eviction).
Self-maintained (periodic cleanup of expired entries / stale keys).
Proposed changes
Pick one:
Protect the cache map with a
sync.RWMutex(preferred if we implement LRU since we’ll need structured operations anyway).Or replace with
sync.Map(but LRU/eviction is harder to do correctly withsync.Map, so likely not the best fit if we add LRU).Implement a bounded cache with eviction, ideally LRU:
Configurable
maxEntries(default value if config not provided).On insert when size exceeds
maxEntries, evict the least-recently-used entries.Consider TTL per entry (optional but recommended) to prevent stale data.
Implementation options:
Small internal LRU (map + doubly-linked list via
container/list)Or use a well-known Go LRU library (only if adding a dependency is acceptable).
Add a periodic cleanup mechanism:
A background goroutine with a
time.Ticker(e.g. every N minutes) that prunes expired entries.Ensure it stops cleanly on shutdown (context cancellation / stop channel).
Acceptance criteria
✅
go test -race ./...shows no data races under load.✅ Cache cannot exceed
maxEntries(validated by unit tests).✅ Eviction behaves like LRU (unit tests: access updates recency, insert triggers expected eviction).
✅ Periodic cleanup removes expired entries (unit test with fake clock or controllable time source if possible).
✅ No goroutine leaks (cleanup ticker stops on shutdown).
Notes / hints
If LRU is implemented, operations typically require a lock anyway →
sync.RWMutex+ explicit LRU structure is straightforward.If the cache is read-heavy, consider
RLockfor reads, but note that LRU “touch” on read requires upgrading to write; simplest approach is just a singleLockfor Get/Set if performance is not critical.Tasks checklist
Identify cache structure in
policy.go(around line 14) and all access points.Add locking around cache operations (or refactor to a dedicated cache type).
Implement bounded LRU (
maxEntries) and eviction.Add TTL metadata and periodic cleanup.
Add tests +
run -race.