From e3f43b96ce1a6a9b63992eceace380731828ffcf Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 01:23:19 +0800 Subject: [PATCH] feat: add Cache.Stop to shut down janitor without finalizer --- cache.go | 29 +++++++++++++++++++++++++++-- cache_test.go | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/cache.go b/cache.go index db88d2f..d2615a1 100644 --- a/cache.go +++ b/cache.go @@ -1087,18 +1087,43 @@ func (j *janitor) Run(c *cache) { } func stopJanitor(c *Cache) { - c.janitor.stop <- true + if c == nil || c.cache == nil || c.janitor == nil { + return + } + // Non-blocking send: Stop may already have closed the janitor. + select { + case c.janitor.stop <- true: + default: + } + c.janitor = nil } func runJanitor(c *cache, ci time.Duration) { j := &janitor{ Interval: ci, - stop: make(chan bool), + stop: make(chan bool, 1), } c.janitor = j go j.Run(c) } +// Stop ends the background janitor goroutine, if one is running. +// It is safe to call multiple times. After Stop, expired items are no longer +// removed automatically; callers may still invoke DeleteExpired manually. +// +// Prefer Stop over relying solely on the finalizer when the cache is used in +// tests (including testing/synctest) or short-lived processes that check for +// goroutine leaks. +func (c *Cache) Stop() { + if c == nil || c.cache == nil { + return + } + if c.janitor != nil { + stopJanitor(c) + } + runtime.SetFinalizer(c, nil) +} + func newCache(de time.Duration, m map[string]Item) *cache { if de == 0 { de = -1 diff --git a/cache_test.go b/cache_test.go index de3e9d6..3107d3c 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1769,3 +1769,23 @@ func TestGetWithExpiration(t *testing.T) { t.Error("expiration for e is in the past") } } + + +func TestStopJanitor(t *testing.T) { + tc := New(DefaultExpiration, 50*time.Millisecond) + tc.Set("a", "b", DefaultExpiration) + tc.Stop() + // Second Stop must be a no-op. + tc.Stop() + // Cache remains usable after Stop. + tc.Set("c", "d", DefaultExpiration) + v, found := tc.Get("c") + if !found || v != "d" { + t.Fatalf("expected usable cache after Stop, got %v %v", v, found) + } +} + +func TestStopWithoutJanitor(t *testing.T) { + tc := New(DefaultExpiration, 0) + tc.Stop() // no-op path +}