From 6dad98a783706eed16bd2f90356daa33bbc925b2 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 23 Jul 2013 17:18:04 -0700 Subject: [PATCH] initial commit. --- .gitignore | 1 + LICENSE | 191 +++++++++++++ README.md | 73 +++++ byteview.go | 160 +++++++++++ byteview_test.go | 142 ++++++++++ groupcache.go | 455 ++++++++++++++++++++++++++++++ groupcache.pb.go | 65 +++++ groupcache_test.go | 367 ++++++++++++++++++++++++ groupcachepb/groupcache.pb.go | 65 +++++ groupcachepb/groupcache.proto | 32 +++ lru/lru.go | 121 ++++++++ lru/lru_test.go | 73 +++++ peers.go | 68 +++++ singleflight/singleflight.go | 64 +++++ singleflight/singleflight_test.go | 85 ++++++ sinks.go | 322 +++++++++++++++++++++ testpb/test.pb.go | 235 +++++++++++++++ testpb/test.proto | 61 ++++ 18 files changed, 2580 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 byteview.go create mode 100644 byteview_test.go create mode 100644 groupcache.go create mode 100644 groupcache.pb.go create mode 100644 groupcache_test.go create mode 100644 groupcachepb/groupcache.pb.go create mode 100644 groupcachepb/groupcache.proto create mode 100644 lru/lru.go create mode 100644 lru/lru_test.go create mode 100644 peers.go create mode 100644 singleflight/singleflight.go create mode 100644 singleflight/singleflight_test.go create mode 100644 sinks.go create mode 100644 testpb/test.pb.go create mode 100644 testpb/test.proto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..566857f --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# groupcache + +## Summary + +groupcache is a caching and cache-filling library, intended as a +replacement for memcached in many cases. + +For API docs and examples, see http://godoc.org/github.com/golang/groupcache + +## Comparison to memcached + +### **Like memcached**, groupcache: + + * shards by key to select which peer is responsible for that key + +### **Unlike memcached**, groupcache: + + * does not require running a separate set of servers, thus massively + reducing deployment/configuration pain. groupcache is a client + library as well as a server. It connects to its own peers. + + * comes with a cache filling mechanism. Whereas memcached just says + "Sorry, cache miss", often resulting in a thundering herd of + database (or whatever) loads from an unbounded number of clients + (which has resulted in several fun outages), groupcache coordinates + cache fills such that only one load in one process of an entire + replicated set of processes populates the cache, then multiplexes + the loaded value to all callers. + + * does not support versioned values. If key "foo" is value "bar", + key "foo" must always be "bar". There are neither cache expiration + times, nor explicit cache evictions. Thus there is also no CAS, + nor Increment/Decrement. This also means that groupcache.... + + * ... supports automatic mirroring of super-hot items to multiple + processes. This prevents memcached hot spotting where a machine's + CPU and/or NIC are overloaded by very popular keys/values. + + * is currently only available for Go. It's very unlikely that I + (bradfitz@) will port the code to any other language. + +## Loading process + +In a nutshell, a groupcache lookup of **Get("foo")** looks like: + +(On machine #5 of a set of N machines running the same code) + + 1. Is the value of "foo" in local memory because it's super hot? If so, use it. + + 2. Is the value of "foo" in local memory because peer #5 (the current + peer) is the owner of it? If so, use it. + + 3. Amongst all the peers in my set of N, am I the owner of the key + "foo"? (e.g. does it consistent hash to 5?) If so, load it. If + other callers come in (via the same process or via RPC requests + from peers, they block waiting for the load to finish and get the + same answer). If not, RPC to the peer that's the owner and get + the answer. If the RPC fails, just load it locally (still with + local dup suppresion). + +## Users + +groupcache is in production use by dl.google.com (its original user), +parts of Blogger, parts of Google Code, parts of Google Fiber, parts +of Google production monitoring systems, etc. + +## Presentations + +See http://talks.golang.org/2013/oscon-dl.slide + +## Help + +Use the golang-nuts mailing list for any discussion or questions. diff --git a/byteview.go b/byteview.go new file mode 100644 index 0000000..035a9ee --- /dev/null +++ b/byteview.go @@ -0,0 +1,160 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package groupcache + +import ( + "bytes" + "errors" + "io" + "strings" +) + +// A ByteView holds an immutable view of bytes. +// Internally it wraps either a []byte or a string, +// but that detail is invisible to callers. +// +// A ByteView is meant to be used as a value type, not +// a pointer (like a time.Time). +type ByteView struct { + // If b is non-nil, b is used, else s is used. + b []byte + s string +} + +// Len returns the view's length. +func (v ByteView) Len() int { + if v.b != nil { + return len(v.b) + } + return len(v.s) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + if v.b != nil { + return cloneBytes(v.b) + } + return []byte(v.s) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + if v.b != nil { + return string(v.b) + } + return v.s +} + +// At returns the byte at index i. +func (v ByteView) At(i int) byte { + if v.b != nil { + return v.b[i] + } + return v.s[i] +} + +// Slice slices the view between the provided from and to indices. +func (v ByteView) Slice(from, to int) ByteView { + if v.b != nil { + return ByteView{b: v.b[from:to]} + } + return ByteView{s: v.s[from:to]} +} + +// SliceFrom slices the view from the provided index until the end. +func (v ByteView) SliceFrom(from int) ByteView { + if v.b != nil { + return ByteView{b: v.b[from:]} + } + return ByteView{s: v.s[from:]} +} + +// Copy copies b into dest and returns the number of bytes copied. +func (v ByteView) Copy(dest []byte) int { + if v.b != nil { + return copy(dest, v.b) + } + return copy(dest, v.s) +} + +// Equal returns whether the bytes in b are the same as the bytes in +// b2. +func (v ByteView) Equal(b2 ByteView) bool { + if b2.b == nil { + return v.EqualString(b2.s) + } + return v.EqualBytes(b2.b) +} + +// EqualString returns whether the bytes in b are the same as the bytes +// in s. +func (v ByteView) EqualString(s string) bool { + if v.b == nil { + return v.s == s + } + l := v.Len() + if len(s) != l { + return false + } + for i, bi := range v.b { + if bi != s[i] { + return false + } + } + return true +} + +// EqualBytes returns whether the bytes in b are the same as the bytes +// in b2. +func (v ByteView) EqualBytes(b2 []byte) bool { + if v.b != nil { + return bytes.Equal(v.b, b2) + } + l := v.Len() + if len(b2) != l { + return false + } + for i, bi := range b2 { + if bi != v.s[i] { + return false + } + } + return true +} + +// Reader returns an io.ReadSeeker for the bytes in v. +func (v ByteView) Reader() io.ReadSeeker { + if v.b != nil { + return bytes.NewReader(v.b) + } + return strings.NewReader(v.s) +} + +// ReadAt implements io.ReaderAt on the bytes in v. +func (v ByteView) ReadAt(p []byte, off int64) (n int, err error) { + if off < 0 { + return 0, errors.New("view: invalid offset") + } + if off >= int64(v.Len()) { + return 0, io.EOF + } + n = v.SliceFrom(int(off)).Copy(p) + if n < len(p) { + err = io.EOF + } + return +} diff --git a/byteview_test.go b/byteview_test.go new file mode 100644 index 0000000..9ece00f --- /dev/null +++ b/byteview_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package groupcache + +import ( + "fmt" + "io" + "io/ioutil" + "testing" +) + +func TestByteView(t *testing.T) { + for _, s := range []string{"", "x", "yy"} { + for _, v := range []ByteView{of([]byte(s)), of(s)} { + name := fmt.Sprintf("string %q, view %+v", s, v) + if v.Len() != len(s) { + t.Errorf("%s: Len = %d; want %d", name, v.Len(), len(s)) + } + if v.String() != s { + t.Errorf("%s: String = %q; want %q", name, v.String(), s) + } + var longDest [3]byte + if n := v.Copy(longDest[:]); n != len(s) { + t.Errorf("%s: long Copy = %d; want %d", name, n, len(s)) + } + var shortDest [1]byte + if n := v.Copy(shortDest[:]); n != min(len(s), 1) { + t.Errorf("%s: short Copy = %d; want %d", name, n, min(len(s), 1)) + } + if got, err := ioutil.ReadAll(v.Reader()); err != nil || string(got) != s { + t.Errorf("%s: Reader = %q, %v; want %q", name, got, err, s) + } + if got, err := ioutil.ReadAll(io.NewSectionReader(v, 0, int64(len(s)))); err != nil || string(got) != s { + t.Errorf("%s: SectionReader of ReaderAt = %q, %v; want %q", name, got, err, s) + } + } + } +} + +// of returns a byte view of the []byte or string in x. +func of(x interface{}) ByteView { + if bytes, ok := x.([]byte); ok { + return ByteView{b: bytes} + } + return ByteView{s: x.(string)} +} + +func TestByteViewEqual(t *testing.T) { + tests := []struct { + a interface{} // string or []byte + b interface{} // string or []byte + want bool + }{ + {"x", "x", true}, + {"x", "y", false}, + {"x", "yy", false}, + {[]byte("x"), []byte("x"), true}, + {[]byte("x"), []byte("y"), false}, + {[]byte("x"), []byte("yy"), false}, + {[]byte("x"), "x", true}, + {[]byte("x"), "y", false}, + {[]byte("x"), "yy", false}, + {"x", []byte("x"), true}, + {"x", []byte("y"), false}, + {"x", []byte("yy"), false}, + } + for i, tt := range tests { + va := of(tt.a) + if bytes, ok := tt.b.([]byte); ok { + if got := va.EqualBytes(bytes); got != tt.want { + t.Errorf("%d. EqualBytes = %v; want %v", i, got, tt.want) + } + } else { + if got := va.EqualString(tt.b.(string)); got != tt.want { + t.Errorf("%d. EqualString = %v; want %v", i, got, tt.want) + } + } + if got := va.Equal(of(tt.b)); got != tt.want { + t.Errorf("%d. Equal = %v; want %v", i, got, tt.want) + } + } +} + +func TestByteViewSlice(t *testing.T) { + tests := []struct { + in string + from int + to interface{} // nil to mean the end (SliceFrom); else int + want string + }{ + { + in: "abc", + from: 1, + to: 2, + want: "b", + }, + { + in: "abc", + from: 1, + want: "bc", + }, + { + in: "abc", + to: 2, + want: "ab", + }, + } + for i, tt := range tests { + for _, v := range []ByteView{of([]byte(tt.in)), of(tt.in)} { + name := fmt.Sprintf("test %d, view %+v", i, v) + if tt.to != nil { + v = v.Slice(tt.from, tt.to.(int)) + } else { + v = v.SliceFrom(tt.from) + } + if v.String() != tt.want { + t.Errorf("%s: got %q; want %q", name, v.String(), tt.want) + } + } + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/groupcache.go b/groupcache.go new file mode 100644 index 0000000..ca25fe0 --- /dev/null +++ b/groupcache.go @@ -0,0 +1,455 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package groupcache provides a data loading mechanism with caching +// and de-duplication that works across a set of peer processes. +// +// Each data Get first consults its local cache, otherwise delegates +// to the requested key's canonical owner, which then checks its cache +// or finally gets the data. In the common case, many concurrent +// cache misses across a set of peers for the same key result in just +// one cache fill. +package groupcache + +import ( + "errors" + "math/rand" + "strconv" + "sync" + "sync/atomic" + + pb "github.com/golang/groupcache/groupcachepb" + "github.com/golang/groupcache/lru" + "github.com/golang/groupcache/singleflight" +) + +// A Getter loads data for a key. +type Getter interface { + // Get returns the value identified by key, populating dest. + // + // The returned data must be unversioned. That is, key must + // uniquely describe the loaded data, without an implicit + // current time, and without relying on cache expiration + // mechanisms. + Get(ctx Context, key string, dest Sink) error +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(ctx Context, key string, dest Sink) error + +func (f GetterFunc) Get(ctx Context, key string, dest Sink) error { + return f(ctx, key, dest) +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) + + initPeerServerOnce sync.Once + initPeerServer func() +) + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} + +// NewGroup creates a coordinated group-aware Getter from a Getter. +// +// The returned Getter tries (but does not guarantee) to run only one +// Get call at once for a given key across an entire set of peer +// processes. Concurrent callers both in the local process and in +// other processes receive copies of the answer once the original Get +// completes. +// +// The group name must be unique for each getter. +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + return newGroup(name, cacheBytes, getter, nil) +} + +// If peers is nil, the peerPicker is called via a sync.Once to initialize it. +func newGroup(name string, cacheBytes int64, getter Getter, peers PeerPicker) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + initPeerServerOnce.Do(callInitPeerServer) + if _, dup := groups[name]; dup { + panic("duplicate registration of group " + name) + } + g := &Group{ + name: name, + getter: getter, + peers: peers, + cacheBytes: cacheBytes, + } + if fn := newGroupHook; fn != nil { + fn(g) + } + groups[name] = g + return g +} + +// newGroupHook, if non-nil, is called right after a new group is created. +var newGroupHook func(*Group) + +// RegisterNewGroupHook registers a hook that is run each time +// a group is created. +func RegisterNewGroupHook(fn func(*Group)) { + if newGroupHook != nil { + panic("RegisterNewGroupHook called more than once") + } + newGroupHook = fn +} + +// RegisterServerStart registers a hook that is run when the first +// group is created. +func RegisterServerStart(fn func()) { + if initPeerServer != nil { + panic("RegisterServerStart called more than once") + } + initPeerServer = fn +} + +func callInitPeerServer() { + if initPeerServer != nil { + initPeerServer() + } +} + +// A Group is a cache namespace and associated data loaded spread over +// a group of 1 or more machines. +type Group struct { + name string + getter Getter + peersOnce sync.Once + peers PeerPicker + cacheBytes int64 // limit for sum of mainCache and hotCache size + + // mainCache is a cache of the keys for which this process + // (amongst its peers) is authorative. That is, this cache + // contains keys which consistent hash on to this process's + // peer number. + mainCache cache + + // hotCache contains keys/values for which this peer is not + // authorative (otherwise they would be in mainCache), but + // are popular enough to warrant mirroring in this process to + // avoid going over the network to fetch from a peer. Having + // a hotCache avoids network hotspotting, where a peer's + // network card could become the bottleneck on a popular key. + // This cache is used sparingly to maximize the total number + // of key/value pairs that can stored globally. + hotCache cache + + // loadGroup ensures that each key is only fetched once + // (either locally or remotely), regardless of the number of + // concurrent callers. + loadGroup singleflight.Group + + // Stats are statistics on the group. + Stats Stats +} + +// Stats are per-group statistics. +type Stats struct { + Gets AtomicInt // any Get request, including from peers + CacheHits AtomicInt // either cache was good + PeerLoads AtomicInt // either remote load or remote cache hit (not an error) + PeerErrors AtomicInt + Loads AtomicInt // (gets - cacheHits) + LoadsDeduped AtomicInt // after singleflight + LocalLoads AtomicInt // total good local loads + LocalLoadErrs AtomicInt // total bad local loads + ServerRequests AtomicInt // gets that came over the network from peers +} + +// Name returns the name of the group. +func (g *Group) Name() string { + return g.name +} + +func (g *Group) initPeers() { + if g.peers == nil { + g.peers = getPeers() + } +} + +func (g *Group) Get(ctx Context, key string, dest Sink) error { + g.peersOnce.Do(g.initPeers) + g.Stats.Gets.Add(1) + if dest == nil { + return errors.New("groupcache: nil dest Sink") + } + value, cacheHit := g.lookupCache(key) + + if cacheHit { + g.Stats.CacheHits.Add(1) + return setSinkView(dest, value) + } + + // Optimization to avoid double unmarshalling or copying: keep + // track of whether the dest was already populated. One caller + // (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) + if err != nil { + return err + } + if destPopulated { + return nil + } + return setSinkView(dest, value) +} + +// load loads key either by invoking the getter locally or by sending it to another machine. +func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) { + g.Stats.Loads.Add(1) + viewi, err := g.loadGroup.Do(key, func() (interface{}, error) { + g.Stats.LoadsDeduped.Add(1) + var value ByteView + var err error + if peer, ok := g.peers.PickPeer(key); ok { + value, err = g.getFromPeer(ctx, peer, key) + if err == nil { + g.Stats.PeerLoads.Add(1) + return value, nil + } + g.Stats.PeerErrors.Add(1) + // TODO(bradfitz): log the peer's error? keep + // log of the past few for /groupcachez? It's + // probably boring (normal task movement), so not + // worth logging I imagine. + } + value, err = g.getLocally(ctx, key, dest) + if err != nil { + g.Stats.LocalLoadErrs.Add(1) + return nil, err + } + g.Stats.LocalLoads.Add(1) + destPopulated = true // only one caller of load gets this return value + g.populateCache(key, value, &g.mainCache) + return value, nil + }) + if err == nil { + value = viewi.(ByteView) + } + return +} + +func (g *Group) getLocally(ctx Context, key string, dest Sink) (ByteView, error) { + err := g.getter.Get(ctx, key, dest) + if err != nil { + return ByteView{}, err + } + return dest.view() +} + +func (g *Group) getFromPeer(ctx Context, peer ProtoGetter, key string) (ByteView, error) { + req := &pb.GetRequest{ + Group: &g.name, + Key: &key, + } + res := &pb.GetResponse{} + err := peer.Get(ctx, req, res) + if err != nil { + return ByteView{}, err + } + value := ByteView{b: res.Value} + // TODO(bradfitz): use res.MinuteQps or something smart to + // conditionally populate hotCache. For now just do it some + // percentage of the time. + if rand.Intn(10) == 0 { + g.populateCache(key, value, &g.hotCache) + } + return value, nil +} + +func (g *Group) lookupCache(key string) (value ByteView, ok bool) { + if g.cacheBytes <= 0 { + return + } + value, ok = g.mainCache.get(key) + if ok { + return + } + value, ok = g.hotCache.get(key) + return +} + +func (g *Group) populateCache(key string, value ByteView, cache *cache) { + if g.cacheBytes <= 0 { + return + } + cache.add(key, value) + + // Evict items from cache(s) if necessary. + for { + mainBytes := g.mainCache.bytes() + hotBytes := g.hotCache.bytes() + if mainBytes+hotBytes <= g.cacheBytes { + return + } + + // TODO(bradfitz): this is good-enough-for-now logic. + // It should be something based on measurements and/or + // respecting the costs of different resources. + victim := &g.mainCache + if hotBytes > mainBytes/8 { + victim = &g.hotCache + } + victim.removeOldest() + } +} + +// CacheType represents a type of cache. +type CacheType int + +const ( + // The MainCache is the cache for items that this peer is the + // owner for. + MainCache CacheType = iota + 1 + + // The HotCache is the cache for items that seem popular + // enough to replicate to this node, even though it's not the + // owner. + HotCache +) + +// CacheStats returns stats about the provided cache within the group. +func (g *Group) CacheStats(which CacheType) CacheStats { + switch which { + case MainCache: + return g.mainCache.stats() + case HotCache: + return g.hotCache.stats() + default: + return CacheStats{} + } +} + +// cache is a wrapper around an *lru.Cache that adds synchronization, +// makes values always be ByteView, and counts the size of all keys and +// values. +type cache struct { + mu sync.RWMutex + nbytes int64 // of all keys and values + lru *lru.Cache + nhit, nget int64 + nevict int64 // number of evictions +} + +func (c *cache) stats() CacheStats { + c.mu.RLock() + defer c.mu.RUnlock() + return CacheStats{ + Bytes: c.nbytes, + Items: c.itemsLocked(), + Gets: c.nget, + Hits: c.nhit, + Evictions: c.nevict, + } +} + +func (c *cache) add(key string, value ByteView) { + c.mu.Lock() + defer c.mu.Unlock() + if c.lru == nil { + c.lru = &lru.Cache{ + OnEvicted: func(key lru.Key, value interface{}) { + val := value.(ByteView) + c.nbytes -= int64(len(key.(string))) + int64(val.Len()) + c.nevict++ + }, + } + } + c.lru.Add(key, value) + c.nbytes += int64(len(key)) + int64(value.Len()) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.nget++ + if c.lru == nil { + return + } + vi, ok := c.lru.Get(key) + if !ok { + return + } + c.nhit++ + return vi.(ByteView), true +} + +func (c *cache) removeOldest() { + c.mu.Lock() + defer c.mu.Unlock() + if c.lru != nil { + c.lru.RemoveOldest() + } +} + +func (c *cache) bytes() int64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.nbytes +} + +func (c *cache) items() int64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.itemsLocked() +} + +func (c *cache) itemsLocked() int64 { + if c.lru == nil { + return 0 + } + return int64(c.lru.Len()) +} + +// An AtomicInt is an int64 to be accessed atomically. +type AtomicInt int64 + +// Add atomically adds n to i. +func (i *AtomicInt) Add(n int64) { + atomic.AddInt64((*int64)(i), n) +} + +// Get atomically gets the value of i. +func (i *AtomicInt) Get() int64 { + return atomic.LoadInt64((*int64)(i)) +} + +func (i *AtomicInt) String() string { + return strconv.FormatInt(i.Get(), 10) +} + +// CacheStats are returned by stats accessors on Group. +type CacheStats struct { + Bytes int64 + Items int64 + Gets int64 + Hits int64 + Evictions int64 +} diff --git a/groupcache.pb.go b/groupcache.pb.go new file mode 100644 index 0000000..db12197 --- /dev/null +++ b/groupcache.pb.go @@ -0,0 +1,65 @@ +// Code generated by protoc-gen-go. +// source: groupcache.proto +// DO NOT EDIT! + +package groupcache + +import proto "code.google.com/p/goprotobuf/proto" +import json "encoding/json" +import math "math" + +// Reference proto, json, and math imports to suppress error if they are not otherwise used. +var _ = proto.Marshal +var _ = &json.SyntaxError{} +var _ = math.Inf + +type GetRequest struct { + Group *string `protobuf:"bytes,1,req,name=group" json:"group,omitempty"` + Key *string `protobuf:"bytes,2,req,name=key" json:"key,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetRequest) Reset() { *m = GetRequest{} } +func (m *GetRequest) String() string { return proto.CompactTextString(m) } +func (*GetRequest) ProtoMessage() {} + +func (m *GetRequest) GetGroup() string { + if m != nil && m.Group != nil { + return *m.Group + } + return "" +} + +func (m *GetRequest) GetKey() string { + if m != nil && m.Key != nil { + return *m.Key + } + return "" +} + +type GetResponse struct { + Value []byte `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` + MinuteQps *float64 `protobuf:"fixed64,2,opt,name=minute_qps" json:"minute_qps,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetResponse) Reset() { *m = GetResponse{} } +func (m *GetResponse) String() string { return proto.CompactTextString(m) } +func (*GetResponse) ProtoMessage() {} + +func (m *GetResponse) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *GetResponse) GetMinuteQps() float64 { + if m != nil && m.MinuteQps != nil { + return *m.MinuteQps + } + return 0 +} + +func init() { +} diff --git a/groupcache_test.go b/groupcache_test.go new file mode 100644 index 0000000..2bfe986 --- /dev/null +++ b/groupcache_test.go @@ -0,0 +1,367 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Tests for groupcache. + +package groupcache + +import ( + "errors" + "fmt" + "hash/crc32" + "math/rand" + "reflect" + "sync" + "testing" + "time" + + "code.google.com/p/goprotobuf/proto" + + pb "github.com/golang/groupcache/groupcachepb" + testpb "github.com/golang/groupcache/testpb" +) + +var ( + once sync.Once + stringGroup, protoGroup Getter + + stringc = make(chan string) + + dummyCtx Context + + // cacheFills is the number of times stringGroup or + // protoGroup's Getter have been called. Read using the + // cacheFills function. + cacheFills AtomicInt +) + +const ( + stringGroupName = "string-group" + protoGroupName = "proto-group" + testMessageType = "google3/net/groupcache/go/test_proto.TestMessage" + fromChan = "from-chan" + cacheSize = 1 << 20 +) + +func testSetup() { + stringGroup = NewGroup(stringGroupName, cacheSize, GetterFunc(func(_ Context, key string, dest Sink) error { + if key == fromChan { + key = <-stringc + } + cacheFills.Add(1) + return dest.SetString("ECHO:" + key) + })) + + protoGroup = NewGroup(protoGroupName, cacheSize, GetterFunc(func(_ Context, key string, dest Sink) error { + if key == fromChan { + key = <-stringc + } + cacheFills.Add(1) + return dest.SetProto(&testpb.TestMessage{ + Name: proto.String("ECHO:" + key), + City: proto.String("SOME-CITY"), + }) + })) +} + +// tests that a Getter's Get method is only called once with two +// outstanding callers. This is the string variant. +func TestGetDupSuppressString(t *testing.T) { + once.Do(testSetup) + // Start two getters. The first should block (waiting reading + // from stringc) and the second should latch on to the first + // one. + resc := make(chan string, 2) + for i := 0; i < 2; i++ { + go func() { + var s string + if err := stringGroup.Get(dummyCtx, fromChan, StringSink(&s)); err != nil { + resc <- "ERROR:" + err.Error() + return + } + resc <- s + }() + } + + // Wait a bit so both goroutines get merged together via + // singleflight. + // TODO(bradfitz): decide whether there are any non-offensive + // debug/test hooks that could be added to singleflight to + // make a sleep here unnecessary. + time.Sleep(250 * time.Millisecond) + + // Unblock the first getter, which should unblock the second + // as well. + stringc <- "foo" + + for i := 0; i < 2; i++ { + select { + case v := <-resc: + if v != "ECHO:foo" { + t.Errorf("got %q; want %q", v, "ECHO:foo") + } + case <-time.After(5 * time.Second): + t.Errorf("timeout waiting on getter #%d of 2", i+1) + } + } +} + +// tests that a Getter's Get method is only called once with two +// outstanding callers. This is the proto variant. +func TestGetDupSuppressProto(t *testing.T) { + once.Do(testSetup) + // Start two getters. The first should block (waiting reading + // from stringc) and the second should latch on to the first + // one. + resc := make(chan *testpb.TestMessage, 2) + for i := 0; i < 2; i++ { + go func() { + tm := new(testpb.TestMessage) + if err := protoGroup.Get(dummyCtx, fromChan, ProtoSink(tm)); err != nil { + tm.Name = proto.String("ERROR:" + err.Error()) + } + resc <- tm + }() + } + + // Wait a bit so both goroutines get merged together via + // singleflight. + // TODO(bradfitz): decide whether there are any non-offensive + // debug/test hooks that could be added to singleflight to + // make a sleep here unnecessary. + time.Sleep(250 * time.Millisecond) + + // Unblock the first getter, which should unblock the second + // as well. + stringc <- "Fluffy" + want := &testpb.TestMessage{ + Name: proto.String("ECHO:Fluffy"), + City: proto.String("SOME-CITY"), + } + for i := 0; i < 2; i++ { + select { + case v := <-resc: + if !reflect.DeepEqual(v, want) { + t.Errorf(" Got: %v\nWant: %v", proto.CompactTextString(v), proto.CompactTextString(want)) + } + case <-time.After(5 * time.Second): + t.Errorf("timeout waiting on getter #%d of 2", i+1) + } + } +} + +func countFills(f func()) int64 { + fills0 := cacheFills.Get() + f() + return cacheFills.Get() - fills0 +} + +func TestCaching(t *testing.T) { + once.Do(testSetup) + fills := countFills(func() { + for i := 0; i < 10; i++ { + var s string + if err := stringGroup.Get(dummyCtx, "TestCaching-key", StringSink(&s)); err != nil { + t.Fatal(err) + } + } + }) + if fills != 1 { + t.Errorf("expected 1 cache fill; got %d", fills) + } +} + +func TestCacheEviction(t *testing.T) { + once.Do(testSetup) + testKey := "TestCacheEviction-key" + getTestKey := func() { + var res string + for i := 0; i < 10; i++ { + if err := stringGroup.Get(dummyCtx, testKey, StringSink(&res)); err != nil { + t.Fatal(err) + } + } + } + fills := countFills(getTestKey) + if fills != 1 { + t.Fatalf("expected 1 cache fill; got %d", fills) + } + + g := stringGroup.(*Group) + evict0 := g.mainCache.nevict + + // Trash the cache with other keys. + var bytesFlooded int64 + // cacheSize/len(testKey) is approximate + for bytesFlooded < cacheSize+1024 { + var res string + key := fmt.Sprintf("dummy-key-%d", bytesFlooded) + stringGroup.Get(dummyCtx, key, StringSink(&res)) + bytesFlooded += int64(len(key) + len(res)) + } + evicts := g.mainCache.nevict - evict0 + if evicts <= 0 { + t.Errorf("evicts = %v; want more than 0", evicts) + } + + // Test that the key is gone. + fills = countFills(getTestKey) + if fills != 1 { + t.Fatalf("expected 1 cache fill after cache trashing; got %d", fills) + } +} + +type fakePeer struct { + hits int + fail bool +} + +func (p *fakePeer) Get(_ Context, in *pb.GetRequest, out *pb.GetResponse) error { + p.hits++ + if p.fail { + return errors.New("simulated error from peer") + } + out.Value = []byte("got:" + in.GetKey()) + return nil +} + +type fakePeers []ProtoGetter + +func (p fakePeers) PickPeer(key string) (peer ProtoGetter, ok bool) { + if len(p) == 0 { + return + } + n := crc32.Checksum([]byte(key), crc32.IEEETable) % uint32(len(p)) + return p[n], p[n] != nil +} + +// tests that peers (virtual, in-process) are hit, and how much. +func TestPeers(t *testing.T) { + once.Do(testSetup) + rand.Seed(123) + peer0 := &fakePeer{} + peer1 := &fakePeer{} + peer2 := &fakePeer{} + peerList := fakePeers([]ProtoGetter{peer0, peer1, peer2, nil}) + const cacheSize = 0 // disabled + localHits := 0 + getter := func(_ Context, key string, dest Sink) error { + localHits++ + return dest.SetString("got:" + key) + } + testGroup := newGroup("TestPeers-group", cacheSize, GetterFunc(getter), peerList) + run := func(name string, n int, wantSummary string) { + // Reset counters + localHits = 0 + for _, p := range []*fakePeer{peer0, peer1, peer2} { + p.hits = 0 + } + + for i := 0; i < n; i++ { + key := fmt.Sprintf("key-%d", i) + want := "got:" + key + var got string + err := testGroup.Get(dummyCtx, key, StringSink(&got)) + if err != nil { + t.Errorf("%s: error on key %q: %v", name, key, err) + continue + } + if got != want { + t.Errorf("%s: for key %q, got %q; want %q", name, key, got, want) + } + } + summary := func() string { + return fmt.Sprintf("localHits = %d, peers = %d %d %d", localHits, peer0.hits, peer1.hits, peer2.hits) + } + if got := summary(); got != wantSummary { + t.Errorf("%s: got %q; want %q", name, got, wantSummary) + } + } + resetCacheSize := func(maxBytes int64) { + g := testGroup + g.cacheBytes = maxBytes + g.mainCache = cache{} + g.hotCache = cache{} + } + + // Base case; peers all up, with no problems. + resetCacheSize(1 << 20) + run("base", 200, "localHits = 49, peers = 51 49 51") + + // Verify cache was hit. All localHits are gone, and some of + // the peer hits (the ones randomly selected to be maybe hot) + run("cached_base", 200, "localHits = 0, peers = 49 47 48") + resetCacheSize(0) + + // With one of the peers being down. + // TODO(bradfitz): on a peer number being unavailable, the + // consistent hashing should maybe keep trying others to + // spread the load out. Currently it fails back to local + // execution if the first consistent-hash slot is unavailable. + peerList[0] = nil + run("one_peer_down", 200, "localHits = 100, peers = 0 49 51") + + // Failing peer + peerList[0] = peer0 + peer0.fail = true + run("peer0_failing", 200, "localHits = 100, peers = 51 49 51") +} + +func TestTruncatingByteSliceTarget(t *testing.T) { + var buf [100]byte + s := buf[:] + if err := stringGroup.Get(dummyCtx, "short", TruncatingByteSliceSink(&s)); err != nil { + t.Fatal(err) + } + if want := "ECHO:short"; string(s) != want { + t.Errorf("short key got %q; want %q", s, want) + } + + s = buf[:6] + if err := stringGroup.Get(dummyCtx, "truncated", TruncatingByteSliceSink(&s)); err != nil { + t.Fatal(err) + } + if want := "ECHO:t"; string(s) != want { + t.Errorf("truncated key got %q; want %q", s, want) + } +} + +func TestAllocatingByteSliceTarget(t *testing.T) { + var dst []byte + sink := AllocatingByteSliceSink(&dst) + + inBytes := []byte("some bytes") + sink.SetBytes(inBytes) + if want := "some bytes"; string(dst) != want { + t.Errorf("SetBytes resulted in %q; want %q", dst, want) + } + v, err := sink.view() + if err != nil { + t.Fatalf("view after SetBytes failed: %v", err) + } + if &inBytes[0] == &dst[0] { + t.Error("inBytes and dst share memory") + } + if &inBytes[0] == &v.b[0] { + t.Error("inBytes and view share memory") + } + if &dst[0] == &v.b[0] { + t.Error("dst and view share memory") + } +} + +// TODO(bradfitz): port the Google-internal full integration test into here, +// using HTTP requests instead of our RPC system. diff --git a/groupcachepb/groupcache.pb.go b/groupcachepb/groupcache.pb.go new file mode 100644 index 0000000..9da3afc --- /dev/null +++ b/groupcachepb/groupcache.pb.go @@ -0,0 +1,65 @@ +// Code generated by protoc-gen-go. +// source: groupcache.proto +// DO NOT EDIT! + +package groupcachepb + +import proto "code.google.com/p/goprotobuf/proto" +import json "encoding/json" +import math "math" + +// Reference proto, json, and math imports to suppress error if they are not otherwise used. +var _ = proto.Marshal +var _ = &json.SyntaxError{} +var _ = math.Inf + +type GetRequest struct { + Group *string `protobuf:"bytes,1,req,name=group" json:"group,omitempty"` + Key *string `protobuf:"bytes,2,req,name=key" json:"key,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetRequest) Reset() { *m = GetRequest{} } +func (m *GetRequest) String() string { return proto.CompactTextString(m) } +func (*GetRequest) ProtoMessage() {} + +func (m *GetRequest) GetGroup() string { + if m != nil && m.Group != nil { + return *m.Group + } + return "" +} + +func (m *GetRequest) GetKey() string { + if m != nil && m.Key != nil { + return *m.Key + } + return "" +} + +type GetResponse struct { + Value []byte `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` + MinuteQps *float64 `protobuf:"fixed64,2,opt,name=minute_qps" json:"minute_qps,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetResponse) Reset() { *m = GetResponse{} } +func (m *GetResponse) String() string { return proto.CompactTextString(m) } +func (*GetResponse) ProtoMessage() {} + +func (m *GetResponse) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *GetResponse) GetMinuteQps() float64 { + if m != nil && m.MinuteQps != nil { + return *m.MinuteQps + } + return 0 +} + +func init() { +} diff --git a/groupcachepb/groupcache.proto b/groupcachepb/groupcache.proto new file mode 100644 index 0000000..153e851 --- /dev/null +++ b/groupcachepb/groupcache.proto @@ -0,0 +1,32 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package groupcachepb; + +message GetRequest { + required string group = 1; + required string key = 2; // not actually required/guaranteed to be UTF-8 +} + +message GetResponse { + optional bytes value = 1; + optional double minute_qps = 2; +} + +service GroupCache { + rpc Get(GetRequest) returns (GetResponse) { + }; +} diff --git a/lru/lru.go b/lru/lru.go new file mode 100644 index 0000000..cdfe299 --- /dev/null +++ b/lru/lru.go @@ -0,0 +1,121 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package lru implements an LRU cache. +package lru + +import "container/list" + +// Cache is an LRU cache. It is not safe for concurrent access. +type Cache struct { + // MaxEntries is the maximum number of cache entries before + // an item is evicted. Zero means no limit. + MaxEntries int + + // OnEvicted optionally specificies a callback function to be + // executed when an entry is purged from the cache. + OnEvicted func(key Key, value interface{}) + + ll *list.List + cache map[interface{}]*list.Element +} + +// A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators +type Key interface{} + +type entry struct { + key Key + value interface{} +} + +// New creates a new Cache. +// If maxEntries is zero, the cache has no limit and it's assumed +// that eviction is done by the caller. +func New(maxEntries int) *Cache { + return &Cache{ + MaxEntries: maxEntries, + ll: list.New(), + cache: make(map[interface{}]*list.Element), + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key Key, value interface{}) { + if c.cache == nil { + c.cache = make(map[interface{}]*list.Element) + c.ll = list.New() + } + if ee, ok := c.cache[key]; ok { + c.ll.MoveToFront(ee) + ee.Value.(*entry).value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { + c.RemoveOldest() + } +} + +// Get looks up a key's value from the cache. +func (c *Cache) Get(key Key) (value interface{}, ok bool) { + if c.cache == nil { + return + } + if ele, hit := c.cache[key]; hit { + c.ll.MoveToFront(ele) + return ele.Value.(*entry).value, true + } + return +} + +// Remove removes the provided key from the cache. +func (c *Cache) Remove(key Key) { + if c.cache == nil { + return + } + if ele, hit := c.cache[key]; hit { + c.removeElement(ele) + } +} + +// RemoveOldest removes the oldest item from the cache. +func (c *Cache) RemoveOldest() { + if c.cache == nil { + return + } + ele := c.ll.Back() + if ele != nil { + c.removeElement(ele) + } +} + +func (c *Cache) removeElement(e *list.Element) { + c.ll.Remove(e) + kv := e.Value.(*entry) + delete(c.cache, kv.key) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } +} + +// Len returns the number of items in the cache. +func (c *Cache) Len() int { + if c.cache == nil { + return 0 + } + return c.ll.Len() +} diff --git a/lru/lru_test.go b/lru/lru_test.go new file mode 100644 index 0000000..98a2656 --- /dev/null +++ b/lru/lru_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lru + +import ( + "testing" +) + +type simpleStruct struct { + int + string +} + +type complexStruct struct { + int + simpleStruct +} + +var getTests = []struct { + name string + keyToAdd interface{} + keyToGet interface{} + expectedOk bool +}{ + {"string_hit", "myKey", "myKey", true}, + {"string_miss", "myKey", "nonsense", false}, + {"simple_struct_hit", simpleStruct{1, "two"}, simpleStruct{1, "two"}, true}, + {"simeple_struct_miss", simpleStruct{1, "two"}, simpleStruct{0, "noway"}, false}, + {"complex_struct_hit", complexStruct{1, simpleStruct{2, "three"}}, + complexStruct{1, simpleStruct{2, "three"}}, true}, +} + +func TestGet(t *testing.T) { + for _, tt := range getTests { + lru := New(0) + lru.Add(tt.keyToAdd, 1234) + val, ok := lru.Get(tt.keyToGet) + if ok != tt.expectedOk { + t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok) + } else if ok && val != 1234 { + t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val) + } + } +} + +func TestRemove(t *testing.T) { + lru := New(0) + lru.Add("myKey", 1234) + if val, ok := lru.Get("myKey"); !ok { + t.Fatal("TestRemove returned no match") + } else if val != 1234 { + t.Fatalf("TestRemove failed. Expected %d, got %v", 1234, val) + } + + lru.Remove("myKey") + if _, ok := lru.Get("myKey"); ok { + t.Fatal("TestRemove returned a removed entry") + } +} diff --git a/peers.go b/peers.go new file mode 100644 index 0000000..4644640 --- /dev/null +++ b/peers.go @@ -0,0 +1,68 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// peers.go defines how processes find and communicate with their peers. + +package groupcache + +import ( + pb "github.com/golang/groupcache/groupcachepb" +) + +// Context is an opaque value passed through calls to the +// ProtoGetter. It may be nil if your ProtoGetter implementation does +// not require a context. +type Context interface{} + +// ProtoGetter is the interface that must be implemented by a peer. +type ProtoGetter interface { + Get(context Context, in *pb.GetRequest, out *pb.GetResponse) error +} + +// PeerPicker is the interface that must be implemented to locate +// the peer that owns a specific key. +type PeerPicker interface { + PickPeer(key string) (peer ProtoGetter, ok bool) +} + +// NoPeers is an implementation of PeerPicker that never finds a peer. +type NoPeers struct{} + +func (NoPeers) PickPeer(key string) (peer ProtoGetter, ok bool) { return } + +var ( + portPicker func() PeerPicker +) + +// RegisterInitPeers registers the peer initialization function. +// It is called once, when the first group is created. +func RegisterPeerPicker(fn func() PeerPicker) { + if portPicker != nil { + panic("RegisterInitPeers called more than once") + } + portPicker = fn +} + +func getPeers() PeerPicker { + if portPicker == nil { + return NoPeers{} + } + pk := portPicker() + if pk == nil { + pk = NoPeers{} + } + return pk +} diff --git a/singleflight/singleflight.go b/singleflight/singleflight.go new file mode 100644 index 0000000..ff2c2ee --- /dev/null +++ b/singleflight/singleflight.go @@ -0,0 +1,64 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package singleflight provides a duplicate function call suppression +// mechanism. +package singleflight + +import "sync" + +// call is an in-flight or completed Do call +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +// Group represents a class of work and forms a namespace in which +// units of work can be executed with duplicate suppression. +type Group struct { + mu sync.Mutex // protects m + m map[string]*call // lazily initialized +} + +// Do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} diff --git a/singleflight/singleflight_test.go b/singleflight/singleflight_test.go new file mode 100644 index 0000000..4e5881a --- /dev/null +++ b/singleflight/singleflight_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package singleflight + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestDo(t *testing.T) { + var g Group + v, err := g.Do("key", func() (interface{}, error) { + return "bar", nil + }) + if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { + t.Errorf("Do = %v; want %v", got, want) + } + if err != nil { + t.Errorf("Do error = %v", err) + } +} + +func TestDoErr(t *testing.T) { + var g Group + someErr := errors.New("Some error") + v, err := g.Do("key", func() (interface{}, error) { + return nil, someErr + }) + if err != someErr { + t.Errorf("Do error = %v; want someErr", err, someErr) + } + if v != nil { + t.Errorf("unexpected non-nil value %#v", v) + } +} + +func TestDoDupSuppress(t *testing.T) { + var g Group + c := make(chan string) + var calls int32 + fn := func() (interface{}, error) { + atomic.AddInt32(&calls, 1) + return <-c, nil + } + + const n = 10 + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + v, err := g.Do("key", fn) + if err != nil { + t.Errorf("Do error: %v", err) + } + if v.(string) != "bar" { + t.Errorf("got %q; want %q", v, "bar") + } + wg.Done() + }() + } + time.Sleep(100 * time.Millisecond) // let goroutines above block + c <- "bar" + wg.Wait() + if got := atomic.LoadInt32(&calls); got != 1 { + t.Errorf("number of calls = %d; want 1", got) + } +} diff --git a/sinks.go b/sinks.go new file mode 100644 index 0000000..964d0cc --- /dev/null +++ b/sinks.go @@ -0,0 +1,322 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package groupcache + +import ( + "errors" + + "code.google.com/p/goprotobuf/proto" +) + +// A Sink receives data from a Get call. +// +// Implementation of Getter must call exactly one of the Set methods +// on success. +type Sink interface { + // SetString sets the value to s. + SetString(s string) error + + // SetBytes sets the value to the contents of v. + // The caller retains ownership of v. + SetBytes(v []byte) error + + // SetProto sets the value to the encoded version of m. + // The caller retains ownership of m. + SetProto(m proto.Message) error + + // view returns a frozen view of the bytes for caching. + view() (ByteView, error) +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func setSinkView(s Sink, v ByteView) error { + // A viewSetter is a Sink that can also receive its value from + // a ByteView. This is a fast path to minimize copies when the + // item was already cached locally in memory (where it's + // cached as a ByteView) + type viewSetter interface { + setView(v ByteView) error + } + if vs, ok := s.(viewSetter); ok { + return vs.setView(v) + } + if v.b != nil { + return s.SetBytes(v.b) + } + return s.SetString(v.s) +} + +// StringSink returns a Sink that populates the provided string pointer. +func StringSink(sp *string) Sink { + return &stringSink{sp: sp} +} + +type stringSink struct { + sp *string + v ByteView + // TODO(bradfitz): track whether any Sets were called. +} + +func (s *stringSink) view() (ByteView, error) { + // TODO(bradfitz): return an error if no Set was called + return s.v, nil +} + +func (s *stringSink) SetString(v string) error { + s.v.b = nil + s.v.s = v + *s.sp = v + return nil +} + +func (s *stringSink) SetBytes(v []byte) error { + return s.SetString(string(v)) +} + +func (s *stringSink) SetProto(m proto.Message) error { + b, err := proto.Marshal(m) + if err != nil { + return err + } + s.v.b = b + *s.sp = string(b) + return nil +} + +// ByteViewSink returns a Sink that populates a ByteView. +func ByteViewSink(dst *ByteView) Sink { + if dst == nil { + panic("nil dst") + } + return &byteViewSink{dst: dst} +} + +type byteViewSink struct { + dst *ByteView + + // if this code ever ends up tracking that at least one set* + // method was called, don't make it an error to call set + // methods multiple times. Lorry's payload.go does that, and + // it makes sense. The comment at the top of this file about + // "exactly one of the Set methods" is overly strict. We + // really care about at least once (in a handler), but if + // multiple handlers fail (or multiple functions in a program + // using a Sink), it's okay to re-use the same one. +} + +func (s *byteViewSink) setView(v ByteView) error { + *s.dst = v + return nil +} + +func (s *byteViewSink) view() (ByteView, error) { + return *s.dst, nil +} + +func (s *byteViewSink) SetProto(m proto.Message) error { + b, err := proto.Marshal(m) + if err != nil { + return err + } + *s.dst = ByteView{b: b} + return nil +} + +func (s *byteViewSink) SetBytes(b []byte) error { + *s.dst = ByteView{b: cloneBytes(b)} + return nil +} + +func (s *byteViewSink) SetString(v string) error { + *s.dst = ByteView{s: v} + return nil +} + +// ProtoSink returns a sink that unmarshals binary proto values into m. +func ProtoSink(m proto.Message) Sink { + return &protoSink{ + dst: m, + } +} + +type protoSink struct { + dst proto.Message // authorative value + typ string + + v ByteView // encoded +} + +func (s *protoSink) view() (ByteView, error) { + return s.v, nil +} + +func (s *protoSink) SetBytes(b []byte) error { + err := proto.Unmarshal(b, s.dst) + if err != nil { + return err + } + s.v.b = cloneBytes(b) + s.v.s = "" + return nil +} + +func (s *protoSink) SetString(v string) error { + b := []byte(v) + err := proto.Unmarshal(b, s.dst) + if err != nil { + return err + } + s.v.b = b + s.v.s = "" + return nil +} + +func (s *protoSink) SetProto(m proto.Message) error { + b, err := proto.Marshal(m) + if err != nil { + return err + } + // TODO(bradfitz): optimize for same-task case more and write + // right through? would need to document ownership rules at + // the same time. but then we could just assign *dst = *m + // here. This works for now: + err = proto.Unmarshal(b, s.dst) + if err != nil { + return err + } + s.v.b = b + s.v.s = "" + return nil +} + +// AllocatingByteSliceSink returns a Sink that allocates +// a byte slice to hold the received value and assigns +// it to *dst. The memory is not retained by groupcache. +func AllocatingByteSliceSink(dst *[]byte) Sink { + return &allocBytesSink{dst: dst} +} + +type allocBytesSink struct { + dst *[]byte + v ByteView +} + +func (s *allocBytesSink) view() (ByteView, error) { + return s.v, nil +} + +func (s *allocBytesSink) setView(v ByteView) error { + if v.b != nil { + *s.dst = cloneBytes(v.b) + } else { + *s.dst = []byte(v.s) + } + s.v = v + return nil +} + +func (s *allocBytesSink) SetProto(m proto.Message) error { + b, err := proto.Marshal(m) + if err != nil { + return err + } + return s.setBytesOwned(b) +} + +func (s *allocBytesSink) SetBytes(b []byte) error { + return s.setBytesOwned(cloneBytes(b)) +} + +func (s *allocBytesSink) setBytesOwned(b []byte) error { + if s.dst == nil { + return errors.New("nil AllocatingByteSliceSink *[]byte dst") + } + *s.dst = cloneBytes(b) // another copy, protecting the read-only s.v.b view + s.v.b = b + s.v.s = "" + return nil +} + +func (s *allocBytesSink) SetString(v string) error { + if s.dst == nil { + return errors.New("nil AllocatingByteSliceSink *[]byte dst") + } + *s.dst = []byte(v) + s.v.b = nil + s.v.s = v + return nil +} + +// TruncatingByteSliceSink returns a Sink that writes up to len(*dst) +// bytes to *dst. If more bytes are available, they're silently +// truncated. If fewer bytes are available than len(*dst), *dst +// is shrunk to fit the number of bytes available. +func TruncatingByteSliceSink(dst *[]byte) Sink { + return &truncBytesSink{dst: dst} +} + +type truncBytesSink struct { + dst *[]byte + v ByteView +} + +func (s *truncBytesSink) view() (ByteView, error) { + return s.v, nil +} + +func (s *truncBytesSink) SetProto(m proto.Message) error { + b, err := proto.Marshal(m) + if err != nil { + return err + } + return s.setBytesOwned(b) +} + +func (s *truncBytesSink) SetBytes(b []byte) error { + return s.setBytesOwned(cloneBytes(b)) +} + +func (s *truncBytesSink) setBytesOwned(b []byte) error { + if s.dst == nil { + return errors.New("nil TruncatingByteSliceSink *[]byte dst") + } + n := copy(*s.dst, b) + if n < len(*s.dst) { + *s.dst = (*s.dst)[:n] + } + s.v.b = b + s.v.s = "" + return nil +} + +func (s *truncBytesSink) SetString(v string) error { + if s.dst == nil { + return errors.New("nil TruncatingByteSliceSink *[]byte dst") + } + n := copy(*s.dst, v) + if n < len(*s.dst) { + *s.dst = (*s.dst)[:n] + } + s.v.b = nil + s.v.s = v + return nil +} diff --git a/testpb/test.pb.go b/testpb/test.pb.go new file mode 100644 index 0000000..ec666b0 --- /dev/null +++ b/testpb/test.pb.go @@ -0,0 +1,235 @@ +// Code generated by protoc-gen-go. +// source: test.proto +// DO NOT EDIT! + +package testpb + +import proto "code.google.com/p/goprotobuf/proto" +import json "encoding/json" +import math "math" + +// Reference proto, json, and math imports to suppress error if they are not otherwise used. +var _ = proto.Marshal +var _ = &json.SyntaxError{} +var _ = math.Inf + +type TestMessage struct { + Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + City *string `protobuf:"bytes,2,opt,name=city" json:"city,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *TestMessage) Reset() { *m = TestMessage{} } +func (m *TestMessage) String() string { return proto.CompactTextString(m) } +func (*TestMessage) ProtoMessage() {} + +func (m *TestMessage) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *TestMessage) GetCity() string { + if m != nil && m.City != nil { + return *m.City + } + return "" +} + +type TestRequest struct { + Lower *string `protobuf:"bytes,1,req,name=lower" json:"lower,omitempty"` + RepeatCount *int32 `protobuf:"varint,2,opt,name=repeat_count,def=1" json:"repeat_count,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *TestRequest) Reset() { *m = TestRequest{} } +func (m *TestRequest) String() string { return proto.CompactTextString(m) } +func (*TestRequest) ProtoMessage() {} + +const Default_TestRequest_RepeatCount int32 = 1 + +func (m *TestRequest) GetLower() string { + if m != nil && m.Lower != nil { + return *m.Lower + } + return "" +} + +func (m *TestRequest) GetRepeatCount() int32 { + if m != nil && m.RepeatCount != nil { + return *m.RepeatCount + } + return Default_TestRequest_RepeatCount +} + +type TestResponse struct { + Value *string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *TestResponse) Reset() { *m = TestResponse{} } +func (m *TestResponse) String() string { return proto.CompactTextString(m) } +func (*TestResponse) ProtoMessage() {} + +func (m *TestResponse) GetValue() string { + if m != nil && m.Value != nil { + return *m.Value + } + return "" +} + +type CacheStats struct { + Items *int64 `protobuf:"varint,1,opt,name=items" json:"items,omitempty"` + Bytes *int64 `protobuf:"varint,2,opt,name=bytes" json:"bytes,omitempty"` + Gets *int64 `protobuf:"varint,3,opt,name=gets" json:"gets,omitempty"` + Hits *int64 `protobuf:"varint,4,opt,name=hits" json:"hits,omitempty"` + Evicts *int64 `protobuf:"varint,5,opt,name=evicts" json:"evicts,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CacheStats) Reset() { *m = CacheStats{} } +func (m *CacheStats) String() string { return proto.CompactTextString(m) } +func (*CacheStats) ProtoMessage() {} + +func (m *CacheStats) GetItems() int64 { + if m != nil && m.Items != nil { + return *m.Items + } + return 0 +} + +func (m *CacheStats) GetBytes() int64 { + if m != nil && m.Bytes != nil { + return *m.Bytes + } + return 0 +} + +func (m *CacheStats) GetGets() int64 { + if m != nil && m.Gets != nil { + return *m.Gets + } + return 0 +} + +func (m *CacheStats) GetHits() int64 { + if m != nil && m.Hits != nil { + return *m.Hits + } + return 0 +} + +func (m *CacheStats) GetEvicts() int64 { + if m != nil && m.Evicts != nil { + return *m.Evicts + } + return 0 +} + +type StatsResponse struct { + Gets *int64 `protobuf:"varint,1,opt,name=gets" json:"gets,omitempty"` + CacheHits *int64 `protobuf:"varint,12,opt,name=cache_hits" json:"cache_hits,omitempty"` + Fills *int64 `protobuf:"varint,2,opt,name=fills" json:"fills,omitempty"` + TotalAlloc *uint64 `protobuf:"varint,3,opt,name=total_alloc" json:"total_alloc,omitempty"` + MainCache *CacheStats `protobuf:"bytes,4,opt,name=main_cache" json:"main_cache,omitempty"` + HotCache *CacheStats `protobuf:"bytes,5,opt,name=hot_cache" json:"hot_cache,omitempty"` + ServerIn *int64 `protobuf:"varint,6,opt,name=server_in" json:"server_in,omitempty"` + Loads *int64 `protobuf:"varint,8,opt,name=loads" json:"loads,omitempty"` + PeerLoads *int64 `protobuf:"varint,9,opt,name=peer_loads" json:"peer_loads,omitempty"` + PeerErrors *int64 `protobuf:"varint,10,opt,name=peer_errors" json:"peer_errors,omitempty"` + LocalLoads *int64 `protobuf:"varint,11,opt,name=local_loads" json:"local_loads,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *StatsResponse) Reset() { *m = StatsResponse{} } +func (m *StatsResponse) String() string { return proto.CompactTextString(m) } +func (*StatsResponse) ProtoMessage() {} + +func (m *StatsResponse) GetGets() int64 { + if m != nil && m.Gets != nil { + return *m.Gets + } + return 0 +} + +func (m *StatsResponse) GetCacheHits() int64 { + if m != nil && m.CacheHits != nil { + return *m.CacheHits + } + return 0 +} + +func (m *StatsResponse) GetFills() int64 { + if m != nil && m.Fills != nil { + return *m.Fills + } + return 0 +} + +func (m *StatsResponse) GetTotalAlloc() uint64 { + if m != nil && m.TotalAlloc != nil { + return *m.TotalAlloc + } + return 0 +} + +func (m *StatsResponse) GetMainCache() *CacheStats { + if m != nil { + return m.MainCache + } + return nil +} + +func (m *StatsResponse) GetHotCache() *CacheStats { + if m != nil { + return m.HotCache + } + return nil +} + +func (m *StatsResponse) GetServerIn() int64 { + if m != nil && m.ServerIn != nil { + return *m.ServerIn + } + return 0 +} + +func (m *StatsResponse) GetLoads() int64 { + if m != nil && m.Loads != nil { + return *m.Loads + } + return 0 +} + +func (m *StatsResponse) GetPeerLoads() int64 { + if m != nil && m.PeerLoads != nil { + return *m.PeerLoads + } + return 0 +} + +func (m *StatsResponse) GetPeerErrors() int64 { + if m != nil && m.PeerErrors != nil { + return *m.PeerErrors + } + return 0 +} + +func (m *StatsResponse) GetLocalLoads() int64 { + if m != nil && m.LocalLoads != nil { + return *m.LocalLoads + } + return 0 +} + +type Empty struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *Empty) Reset() { *m = Empty{} } +func (m *Empty) String() string { return proto.CompactTextString(m) } +func (*Empty) ProtoMessage() {} + +func init() { +} diff --git a/testpb/test.proto b/testpb/test.proto new file mode 100644 index 0000000..7292dce --- /dev/null +++ b/testpb/test.proto @@ -0,0 +1,61 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testpb; + +message TestMessage { + optional string name = 1; + optional string city = 2; +} + +message TestRequest { + required string lower = 1; // to be returned upper case + optional int32 repeat_count = 2 [default = 1]; // .. this many times +} + +message TestResponse { + optional string value = 1; +} + +message CacheStats { + optional int64 items = 1; + optional int64 bytes = 2; + optional int64 gets = 3; + optional int64 hits = 4; + optional int64 evicts = 5; +} + +message StatsResponse { + optional int64 gets = 1; + optional int64 cache_hits = 12; + optional int64 fills = 2; + optional uint64 total_alloc = 3; + optional CacheStats main_cache = 4; + optional CacheStats hot_cache = 5; + optional int64 server_in = 6; + optional int64 loads = 8; + optional int64 peer_loads = 9; + optional int64 peer_errors = 10; + optional int64 local_loads = 11; +} + +message Empty {} + +service GroupCacheTest { + rpc InitPeers(Empty) returns (Empty) {}; + rpc Get(TestRequest) returns (TestResponse) {}; + rpc GetStats(Empty) returns (StatsResponse) {}; +}