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.