Skip to content

Commit 48a0ce2

Browse files
committed
Get() now returns immediately when context is done
* `Get()` now returns immediately when context is done during a groupcache peer conversation. Previously `Get()` would call the `Getter` with a done context.
1 parent b9bcb40 commit 48a0ce2

8 files changed

+338
-25
lines changed

CHANGELOG

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.1.0] - 2019-06-10
8+
### Changes
9+
* `Get()` now returns immediately when context is done during a groupcache peer
10+
conversation. Previously `Get()` would call the `Getter` with a done context.
11+
712
## [2.0.0] - 2019-06-04
813
### Changes
914
* Now using golang standard `context.Context` instead of `groupcache.Context`.

README.md

+105-23
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
11
# groupcache
22

3+
A modified version of [group cache](https://github.com/golang/groupcache) with
4+
support for `context.Context`, [go modules](https://github.com/golang/go/wiki/Modules),
5+
and explicit key removal and expiration. See the `CHANGELOG` for a complete list of
6+
modifications.
7+
38
## Summary
49

510
groupcache is a caching and cache-filling library, intended as a
611
replacement for memcached in many cases.
712

813
For API docs and examples, see http://godoc.org/github.com/mailgun/groupcache
914

10-
## Comparison to memcached
15+
16+
### Modifications from original library
17+
18+
* Support for explicit key removal from a group. `Remove()` requests are
19+
first sent to the peer who owns the key, then the remove request is
20+
forwarded to every peer in the groupcache. NOTE: This is a best case design
21+
since it is possible a temporary network disruption could occur resulting
22+
in remove requests never making it their peers. In practice this scenario
23+
is very rare and the system remains very consistent. In case of an
24+
inconsistency placing a expiration time on your values will ensure the
25+
cluster eventually becomes consistent again.
26+
27+
* Support for expired values. `SetBytes()`, `SetProto()` and `SetString()` now
28+
accept an optional `time.Time{}` which represents a time in the future when the
29+
value will expire. Expiration is handled by the LRU Cache when a `Get()` on a
30+
key is requested. This means no network coordination of expired values is needed.
31+
However this does require that time on all nodes in the cluster is synchronized
32+
for consistent expiration of values.
33+
34+
* Network methods now accept golang standard `context.Context` instead of
35+
`groupcache.Context`.
36+
37+
* Now always populating the hotcache. A more complex algorithm is unnecessary
38+
when the LRU cache will ensure the most used values remain in the cache. The
39+
evict code ensures the hotcache never overcrowds the maincache.
40+
41+
## Comparing Groupcache to memcached
1142

1243
### **Like memcached**, groupcache:
1344

@@ -28,16 +59,7 @@ For API docs and examples, see http://godoc.org/github.com/mailgun/groupcache
2859
the loaded value to all callers.
2960

3061
* does not support versioned values. If key "foo" is value "bar",
31-
key "foo" must always be "bar". There are neither cache expiration
32-
times, nor explicit cache evictions. Thus there is also no CAS,
33-
nor Increment/Decrement. This also means that groupcache....
34-
35-
* ... supports automatic mirroring of super-hot items to multiple
36-
processes. This prevents memcached hot spotting where a machine's
37-
CPU and/or NIC are overloaded by very popular keys/values.
38-
39-
* is currently only available for Go. It's very unlikely that I
40-
(bradfitz@) will port the code to any other language.
62+
key "foo" must always be "bar".
4163

4264
## Loading process
4365

@@ -58,16 +80,76 @@ In a nutshell, a groupcache lookup of **Get("foo")** looks like:
5880
the answer. If the RPC fails, just load it locally (still with
5981
local dup suppression).
6082

61-
## Users
62-
63-
groupcache is in production use by dl.google.com (its original user),
64-
parts of Blogger, parts of Google Code, parts of Google Fiber, parts
65-
of Google production monitoring systems, etc.
66-
67-
## Presentations
68-
69-
See http://talks.golang.org/2013/oscon-dl.slide
70-
71-
## Help
83+
## Example
84+
85+
```go
86+
import (
87+
"context"
88+
"fmt"
89+
"log"
90+
"time"
91+
92+
"github.com/mailgun/groupcache/v2"
93+
)
94+
95+
func ExampleUsage() {
96+
// Keep track of peers in our cluster and add our instance to the pool `http://localhost:8080`
97+
pool := groupcache.NewHTTPPoolOpts("http://localhost:8080", &groupcache.HTTPPoolOptions{})
98+
99+
// Add more peers to the cluster
100+
//pool.Set("http://peer1:8080", "http://peer2:8080")
101+
102+
server := http.Server{
103+
Addr: "localhost:8080",
104+
Handler: pool,
105+
}
106+
107+
// Start a HTTP server to listen for peer requests from the groupcache
108+
go func() {
109+
log.Printf("Serving....\n")
110+
if err := server.ListenAndServe(); err != nil {
111+
log.Fatal(err)
112+
}
113+
}()
114+
defer server.Shutdown(context.Background())
115+
116+
// Create a new group cache with a max cache size of 3MB
117+
group := groupcache.NewGroup("users", 3000000, groupcache.GetterFunc(
118+
func(ctx context.Context, id string, dest groupcache.Sink) error {
119+
120+
// Returns a protobuf struct `User`
121+
if user, err := fetchUserFromMongo(ctx, id); err != nil {
122+
return err
123+
}
124+
125+
// Set the user in the groupcache to expire after 5 minutes
126+
if err := dest.SetProto(&user, time.Now().Add(time.Minute*5)); err != nil {
127+
return err
128+
}
129+
return nil
130+
},
131+
))
132+
133+
var user User
134+
135+
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
136+
defer cancel()
137+
138+
if err := group.Get(ctx, "12345", groupcache.ProtoSink(&user)); err != nil {
139+
log.Fatal(err)
140+
}
141+
142+
fmt.Printf("-- User --\n")
143+
fmt.Printf("Id: %s\n", user.Id)
144+
fmt.Printf("Name: %s\n", user.Name)
145+
fmt.Printf("Age: %d\n", user.Age)
146+
fmt.Printf("IsSuper: %t\n", user.IsSuper)
147+
148+
// Remove the key from the groupcache
149+
if err := group.Remove(ctx, "12345"); err != nil {
150+
log.Fatal(err)
151+
}
152+
}
153+
154+
```
72155

73-
Use the golang-nuts mailing list for any discussion or questions.

example_pb_test.go

+88
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package groupcache_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"time"
8+
9+
"github.com/mailgun/groupcache/v2"
10+
)
11+
12+
func ExampleUsage() {
13+
/*
14+
// Keep track of peers in our cluster and add our instance to the pool `http://localhost:8080`
15+
pool := groupcache.NewHTTPPoolOpts("http://localhost:8080", &groupcache.HTTPPoolOptions{})
16+
17+
// Add more peers to the cluster
18+
//pool.Set("http://peer1:8080", "http://peer2:8080")
19+
20+
server := http.Server{
21+
Addr: "localhost:8080",
22+
Handler: pool,
23+
}
24+
25+
// Start a HTTP server to listen for peer requests from the groupcache
26+
go func() {
27+
log.Printf("Serving....\n")
28+
if err := server.ListenAndServe(); err != nil {
29+
log.Fatal(err)
30+
}
31+
}()
32+
defer server.Shutdown(context.Background())
33+
*/
34+
35+
// Create a new group cache with a max cache size of 3MB
36+
group := groupcache.NewGroup("users", 3000000, groupcache.GetterFunc(
37+
func(ctx context.Context, id string, dest groupcache.Sink) error {
38+
39+
// In a real scenario we might fetch the value from a database.
40+
/*if user, err := fetchUserFromMongo(ctx, id); err != nil {
41+
return err
42+
}*/
43+
44+
user := User{
45+
Id: "12345",
46+
Name: "John Doe",
47+
Age: 40,
48+
IsSuper: true,
49+
}
50+
51+
// Set the user in the groupcache to expire after 5 minutes
52+
if err := dest.SetProto(&user, time.Now().Add(time.Minute*5)); err != nil {
53+
return err
54+
}
55+
return nil
56+
},
57+
))
58+
59+
var user User
60+
61+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
62+
defer cancel()
63+
64+
if err := group.Get(ctx, "12345", groupcache.ProtoSink(&user)); err != nil {
65+
log.Fatal(err)
66+
}
67+
68+
fmt.Printf("-- User --\n")
69+
fmt.Printf("Id: %s\n", user.Id)
70+
fmt.Printf("Name: %s\n", user.Name)
71+
fmt.Printf("Age: %d\n", user.Age)
72+
fmt.Printf("IsSuper: %t\n", user.IsSuper)
73+
74+
/*
75+
// Remove the key from the groupcache
76+
if err := group.Remove(ctx, "12345"); err != nil {
77+
fmt.Printf("Remove Err: %s\n", err)
78+
log.Fatal(err)
79+
}
80+
*/
81+
82+
// Output: -- User --
83+
// Id: 12345
84+
// Name: John Doe
85+
// Age: 40
86+
// IsSuper: true
87+
}

groupcache.go

+5
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ func (g *Group) load(ctx context.Context, key string, dest Sink) (value ByteView
326326
return value, nil
327327
}
328328
g.Stats.PeerErrors.Add(1)
329+
if ctx != nil && ctx.Err() != nil {
330+
// Return here without attempting to get locally
331+
// since the context is no longer valid
332+
return nil, err
333+
}
329334
// TODO(bradfitz): log the peer's error? keep
330335
// log of the past few for /groupcachez? It's
331336
// probably boring (normal task movement), so not

groupcache_test.go

+32-2
Original file line numberDiff line numberDiff line change
@@ -491,5 +491,35 @@ func TestGroupStatsAlignment(t *testing.T) {
491491
}
492492
}
493493

494-
// TODO(bradfitz): port the Google-internal full integration test into here,
495-
// using HTTP requests instead of our RPC system.
494+
type slowPeer struct {
495+
fakePeer
496+
}
497+
498+
func (p *slowPeer) Get(_ context.Context, in *pb.GetRequest, out *pb.GetResponse) error {
499+
time.Sleep(time.Second)
500+
out.Value = []byte("got:" + in.GetKey())
501+
return nil
502+
}
503+
504+
func TestContextDeadlineOnPeer(t *testing.T) {
505+
once.Do(testSetup)
506+
peer0 := &slowPeer{}
507+
peer1 := &slowPeer{}
508+
peer2 := &slowPeer{}
509+
peerList := fakePeers([]ProtoGetter{peer0, peer1, peer2, nil})
510+
getter := func(_ context.Context, key string, dest Sink) error {
511+
return dest.SetString("got:"+key, time.Time{})
512+
}
513+
testGroup := newGroup("TestContextDeadlineOnPeer-group", cacheSize, GetterFunc(getter), peerList)
514+
515+
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*300)
516+
defer cancel()
517+
518+
var got string
519+
err := testGroup.Get(ctx, "test-key", StringSink(&got))
520+
if err != nil {
521+
if err != context.DeadlineExceeded {
522+
t.Errorf("expected Get to return context deadline exceeded")
523+
}
524+
}
525+
}

0 commit comments

Comments
 (0)