From 43c3d4c0d59b88e46f13076189ed05dc988513f7 Mon Sep 17 00:00:00 2001 From: toller892 Date: Tue, 2 Jun 2026 09:42:41 +0800 Subject: [PATCH] feat: add GetCached method to report cache hit status GetCached acts like Get but additionally returns a bool indicating whether the value was served from the local cache. This allows callers to distinguish cache hits from misses without relying on Stats counters. The existing Get method is refactored to delegate to the internal getCached helper, preserving full backward compatibility. Fixes #61 --- groupcache.go | 25 +++++++++++++++++++------ groupcache_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/groupcache.go b/groupcache.go index bc123f1d..312c8f61 100644 --- a/groupcache.go +++ b/groupcache.go @@ -210,16 +210,29 @@ func (g *Group) initPeers() { } func (g *Group) Get(ctx context.Context, key string, dest Sink) error { + _, err := g.getCached(ctx, key, dest) + return err +} + +// GetCached acts like Get but additionally reports whether the +// value was served from the local cache. This is useful for +// callers that want to distinguish cache hits from cache misses +// without relying on the Stats counters. +func (g *Group) GetCached(ctx context.Context, key string, dest Sink) (cached bool, err error) { + return g.getCached(ctx, key, dest) +} + +func (g *Group) getCached(ctx context.Context, key string, dest Sink) (cached bool, err error) { g.peersOnce.Do(g.initPeers) g.Stats.Gets.Add(1) if dest == nil { - return errors.New("groupcache: nil dest Sink") + return false, errors.New("groupcache: nil dest Sink") } value, cacheHit := g.lookupCache(key) if cacheHit { g.Stats.CacheHits.Add(1) - return setSinkView(dest, value) + return true, setSinkView(dest, value) } // Optimization to avoid double unmarshalling or copying: keep @@ -227,14 +240,14 @@ func (g *Group) Get(ctx context.Context, key string, dest Sink) error { // (if local) will set this; the losers will not. The common // case will likely be one caller. destPopulated := false - value, destPopulated, err := g.load(ctx, key, dest) + value, destPopulated, err = g.load(ctx, key, dest) if err != nil { - return err + return false, err } if destPopulated { - return nil + return false, nil } - return setSinkView(dest, value) + return false, setSinkView(dest, value) } // load loads key either by invoking the getter locally or by sending it to another machine. diff --git a/groupcache_test.go b/groupcache_test.go index 1bfe278c..328de17a 100644 --- a/groupcache_test.go +++ b/groupcache_test.go @@ -453,5 +453,47 @@ func TestGroupStatsAlignment(t *testing.T) { } } +// TestGetCached tests that GetCached correctly reports cache hit/miss. +func TestGetCached(t *testing.T) { + once.Do(testSetup) + g := newGroup("TestGetCached-group", 1<<20, GetterFunc(func(_ context.Context, key string, dest Sink) error { + return dest.SetString("val:" + key) + }), nil) + + key := "cached-key" + var s string + + // First call: cache miss. + cached, err := g.GetCached(dummyCtx, key, StringSink(&s)) + if err != nil { + t.Fatal(err) + } + if cached { + t.Error("first GetCached returned cached=true; want false") + } + if s != "val:cached-key" { + t.Errorf("got %q; want %q", s, "val:cached-key") + } + + // Second call: should be a cache hit. + s = "" + cached, err = g.GetCached(dummyCtx, key, StringSink(&s)) + if err != nil { + t.Fatal(err) + } + if !cached { + t.Error("second GetCached returned cached=false; want true") + } + if s != "val:cached-key" { + t.Errorf("got %q; want %q", s, "val:cached-key") + } + + // Nil dest should return error. + _, err = g.GetCached(dummyCtx, "any", nil) + if err == nil { + t.Error("GetCached with nil dest should return error") + } +} + // TODO(bradfitz): port the Google-internal full integration test into here, // using HTTP requests instead of our RPC system.