├── .gitignore ├── docs ├── code-diagram.png ├── groupcache-logo.png ├── simplified-diagram.png └── sequence-diagram.mermaid ├── buf.gen.yaml ├── errors_test.go ├── Makefile ├── errors.go ├── transport ├── types.go ├── pb │ ├── groupcache.proto │ ├── testpb │ │ └── test.proto │ └── groupcache.pb.go ├── errors.go ├── peer │ ├── client.go │ ├── picker.go │ └── picker_test.go ├── tracer.go ├── tls_test.go ├── tracer_test.go ├── byteview_test.go ├── byteview.go ├── mock_transport.go ├── sinks.go └── http_transport_test.go ├── go.mod ├── .github └── workflows │ └── on-pull-request.yaml ├── internal ├── singleflight │ ├── singleflight.go │ └── singleflight_test.go └── lru │ ├── lru.go │ └── lru_test.go ├── contrib ├── otter_test.go └── otter.go ├── cluster ├── cluster_test.go └── cluster.go ├── daemon.go ├── cmd └── server │ └── main.go ├── cache_test.go ├── cache.go ├── go.sum ├── example_test.go ├── instance.go ├── stats_test.go ├── stats.go ├── LICENSE ├── group.go └── instance_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea/ 3 | .DS_Store 4 | vendor 5 | .aider* 6 | .env 7 | .cache/ -------------------------------------------------------------------------------- /docs/code-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groupcache/groupcache-go/HEAD/docs/code-diagram.png -------------------------------------------------------------------------------- /docs/groupcache-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groupcache/groupcache-go/HEAD/docs/groupcache-logo.png -------------------------------------------------------------------------------- /docs/simplified-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groupcache/groupcache-go/HEAD/docs/simplified-diagram.png -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S buf generate --template 2 | --- 3 | version: v1 4 | plugins: 5 | - plugin: buf.build/protocolbuffers/go:v1.32.0 6 | out: ./ 7 | opt: paths=source_relative 8 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package groupcache 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMultiError(t *testing.T) { 11 | m := &MultiError{} 12 | err := m.NilOrError() 13 | assert.NoError(t, err) 14 | 15 | m.Add(errors.New("this one error")) 16 | err = m.NilOrError() 17 | assert.Error(t, err) 18 | assert.Equal(t, "this one error", err.Error()) 19 | 20 | m.Add(errors.New("this a second error")) 21 | err = m.NilOrError() 22 | assert.Error(t, err) 23 | assert.Equal(t, "this one error\nthis a second error", err.Error()) 24 | } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOLANGCI_LINT = $(GOPATH)/bin/golangci-lint 2 | GOLANGCI_LINT_VERSION = v1.61.0 3 | 4 | $(GOLANGCI_LINT): ## Download Go linter 5 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION) 6 | 7 | .PHONY: ci 8 | ci: tidy lint test 9 | @echo 10 | @echo "\033[32mEVERYTHING PASSED!\033[0m" 11 | 12 | .PHONY: lint 13 | lint: $(GOLANGCI_LINT) ## Run Go linter 14 | $(GOLANGCI_LINT) run -v ./... 15 | 16 | .PHONY: tidy 17 | tidy: 18 | go mod tidy && git diff --exit-code 19 | 20 | .PHONY: test 21 | test: 22 | go test ./... 23 | 24 | .PHONY: bench 25 | bench: ## Run Go benchmarks 26 | go test ./... -bench . -benchtime 5s -timeout 0 -run='^$$' -benchmem 27 | 28 | .PHONY: proto 29 | proto: ## Build protos 30 | ./buf.gen.yaml 31 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package groupcache 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type MultiError struct { 8 | errors []error 9 | } 10 | 11 | func (m *MultiError) Add(err error) { 12 | if err != nil { 13 | m.errors = append(m.errors, err) 14 | } 15 | } 16 | 17 | func (m *MultiError) Error() string { 18 | if len(m.errors) == 0 { 19 | return "" 20 | } 21 | 22 | var errStrings []string 23 | for _, err := range m.errors { 24 | errStrings = append(errStrings, err.Error()) 25 | } 26 | return strings.Join(errStrings, "\n") 27 | } 28 | 29 | func (m *MultiError) NilOrError() error { 30 | if len(m.errors) == 0 { 31 | return nil 32 | } 33 | return m 34 | } 35 | 36 | // ErrRemoteCall is returned from `group.Get()` when a remote GetterFunc returns an 37 | // error. When this happens `group.Get()` does not attempt to retrieve the value 38 | // via our local GetterFunc. 39 | type ErrRemoteCall struct { 40 | Msg string 41 | } 42 | -------------------------------------------------------------------------------- /transport/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transport 18 | 19 | import ( 20 | "context" 21 | "time" 22 | ) 23 | 24 | type Group interface { 25 | Set(context.Context, string, []byte, time.Time, bool) error 26 | Get(context.Context, string, Sink) error 27 | Remove(context.Context, string) error 28 | UsedBytes() (int64, int64) 29 | Name() string 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/groupcache/groupcache-go/v3 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/kapetan-io/tackle v0.13.0 7 | github.com/maypok86/otter v1.2.4 8 | github.com/segmentio/fasthash v1.0.3 9 | github.com/stretchr/testify v1.11.1 10 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 11 | go.opentelemetry.io/otel v1.38.0 12 | go.opentelemetry.io/otel/metric v1.38.0 13 | go.opentelemetry.io/otel/sdk v1.38.0 14 | go.opentelemetry.io/otel/trace v1.38.0 15 | golang.org/x/net v0.47.0 16 | google.golang.org/protobuf v1.36.10 17 | ) 18 | 19 | require ( 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/dolthub/maphash v0.1.0 // indirect 22 | github.com/felixge/httpsnoop v1.0.4 // indirect 23 | github.com/gammazero/deque v1.2.0 // indirect 24 | github.com/go-logr/logr v1.4.3 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 29 | golang.org/x/sys v0.38.0 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /transport/pb/groupcache.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto2"; 18 | 19 | package pb; 20 | option go_package = "github.com/groupcache/groupcache-go/pb"; 21 | 22 | message GetRequest { 23 | required string group = 1; 24 | required string key = 2; // not actually required/guaranteed to be UTF-8 25 | } 26 | 27 | message GetResponse { 28 | optional bytes value = 1; 29 | optional double minute_qps = 2; 30 | optional int64 expire = 3; 31 | } 32 | 33 | message SetRequest { 34 | required string group = 1; 35 | required string key = 2; 36 | optional bytes value = 3; 37 | optional int64 expire = 4; 38 | } 39 | 40 | service GroupCache { 41 | rpc Get(GetRequest) returns (GetResponse) { 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /docs/sequence-diagram.mermaid: -------------------------------------------------------------------------------- 1 | --- 2 | theme: base 3 | look: handDrawn 4 | --- 5 | sequenceDiagram 6 | participant App 7 | participant LocalGroupCache 8 | participant PeerGroupCache 9 | participant DataSource 10 | 11 | App->>LocalGroupCache: Request FOO 12 | alt FOO owned by LocalGroupcache 13 | alt FOO found in LocalGroupcache 14 | LocalGroupCache->>App: Return cached data 15 | else FOO not found in LocalGroupcache 16 | LocalGroupCache->>DataSource: Fetch data 17 | DataSource->>LocalGroupCache: Return data 18 | LocalGroupCache->>LocalGroupCache: Store in Main cache 19 | LocalGroupCache->>App: Return data 20 | else FOO owned by PeerGroupCache 21 | LocalGroupCache->>PeerGroupCache: Request FOO from peer 22 | alt FOO found in PeerGroupCache 23 | PeerGroupCache->>LocalGroupCache: Return cached data 24 | LocalGroupCache->>LocalGroupCache: Store data in Hot cache 25 | LocalGroupCache->>App: Return cached data 26 | else FOO not found in PeerGroupCache 27 | PeerGroupCache->>DataSource: Fetch data 28 | DataSource->>PeerGroupCache: Return data 29 | PeerGroupCache->>PeerGroupCache: Store in Local cache 30 | PeerGroupCache->>LocalGroupCache: Return data 31 | LocalGroupCache->>LocalGroupCache: Store in Hot cache 32 | LocalGroupCache->>App: Return data 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /.github/workflows/on-pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | branches: 10 | - master 11 | - main 12 | 13 | jobs: 14 | on-pull-request: 15 | name: test 16 | strategy: 17 | matrix: 18 | go-version: 19 | - 1.24.x 20 | os: [ ubuntu-latest ] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v5 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v6 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | cache: true # caching and restoring go modules and build outputs 31 | 32 | - run: go env 33 | 34 | - name: Cache deps 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/go/pkg/mod 38 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 39 | restore-keys: | 40 | ${{ runner.os }}-go-- 41 | 42 | - name: go mod tidy 43 | run: go mod tidy && git diff --exit-code 44 | 45 | - name: golangci-lint 46 | uses: golangci/golangci-lint-action@v8 47 | with: 48 | version: v2.6.1 49 | skip-cache: true # cache/restore is done by actions/setup-go@v3 step 50 | args: -v 51 | 52 | - name: Install deps 53 | run: go mod download 54 | 55 | - name: Test 56 | run: go test ./... 57 | 58 | -------------------------------------------------------------------------------- /transport/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transport 18 | 19 | import ( 20 | "errors" 21 | ) 22 | 23 | // ErrNotFound should be returned from an implementation of `GetterFunc` to indicate the 24 | // requested value is not available. When remote HTTP calls are made to retrieve values from 25 | // other groupcache instances, returning this error will indicate to groupcache that the 26 | // value requested is not available, and it should NOT attempt to call `GetterFunc` locally. 27 | type ErrNotFound struct { 28 | Msg string 29 | } 30 | 31 | func (e *ErrNotFound) Error() string { 32 | return e.Msg 33 | } 34 | 35 | func (e *ErrNotFound) Is(target error) bool { 36 | var errNotFound *ErrNotFound 37 | return errors.As(target, &errNotFound) 38 | } 39 | 40 | // ErrRemoteCall is returned from `group.Get()` when an error that is not a `ErrNotFound` 41 | // is returned during a remote HTTP instance call 42 | type ErrRemoteCall struct { 43 | Msg string 44 | } 45 | 46 | func (e *ErrRemoteCall) Error() string { 47 | return e.Msg 48 | } 49 | 50 | func (e *ErrRemoteCall) Is(target error) bool { 51 | var errRemoteCall *ErrRemoteCall 52 | return errors.As(target, &errRemoteCall) 53 | } 54 | -------------------------------------------------------------------------------- /transport/peer/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package peer 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/groupcache/groupcache-go/v3/transport/pb" 23 | ) 24 | 25 | // Client is the interface that must be implemented by a peer. 26 | type Client interface { 27 | Get(context context.Context, in *pb.GetRequest, out *pb.GetResponse) error 28 | Remove(context context.Context, in *pb.GetRequest) error 29 | Set(context context.Context, in *pb.SetRequest) error 30 | PeerInfo() Info 31 | HashKey() string 32 | } 33 | 34 | // NoOpClient is used as a placeholder in the picker for the local instance. It is returned 35 | // when `PickPeer()` returns isSelf = true 36 | type NoOpClient struct { 37 | Info Info 38 | } 39 | 40 | func (e *NoOpClient) Get(context context.Context, in *pb.GetRequest, out *pb.GetResponse) error { 41 | return nil 42 | } 43 | 44 | func (e *NoOpClient) Remove(context context.Context, in *pb.GetRequest) error { 45 | return nil 46 | } 47 | 48 | func (e *NoOpClient) Set(context context.Context, in *pb.SetRequest) error { 49 | return nil 50 | } 51 | 52 | func (e *NoOpClient) PeerInfo() Info { 53 | return e.Info 54 | } 55 | 56 | func (e *NoOpClient) HashKey() string { 57 | return e.Info.Address 58 | } 59 | -------------------------------------------------------------------------------- /transport/pb/testpb/test.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto2"; 18 | 19 | package testpb; 20 | option go_package = "github.com/groupcache/groupcache-go/pb/testpb"; 21 | 22 | message TestMessage { 23 | optional string name = 1; 24 | optional string city = 2; 25 | } 26 | 27 | message TestRequest { 28 | required string lower = 1; // to be returned upper case 29 | optional int32 repeat_count = 2 [default = 1]; // .. this many times 30 | } 31 | 32 | message TestResponse { 33 | optional string value = 1; 34 | } 35 | 36 | message CacheStats { 37 | optional int64 items = 1; 38 | optional int64 bytes = 2; 39 | optional int64 gets = 3; 40 | optional int64 hits = 4; 41 | optional int64 evicts = 5; 42 | } 43 | 44 | message StatsResponse { 45 | optional int64 gets = 1; 46 | optional int64 cache_hits = 12; 47 | optional int64 fills = 2; 48 | optional uint64 total_alloc = 3; 49 | optional CacheStats main_cache = 4; 50 | optional CacheStats hot_cache = 5; 51 | optional int64 server_in = 6; 52 | optional int64 loads = 8; 53 | optional int64 peer_loads = 9; 54 | optional int64 peer_errors = 10; 55 | optional int64 local_loads = 11; 56 | } 57 | 58 | message Empty {} 59 | 60 | service GroupCacheTest { 61 | rpc InitPeers(Empty) returns (Empty) {}; 62 | rpc Get(TestRequest) returns (TestResponse) {}; 63 | rpc GetStats(Empty) returns (StatsResponse) {}; 64 | } 65 | -------------------------------------------------------------------------------- /internal/singleflight/singleflight.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package singleflight provides a duplicate function call suppression 18 | // mechanism. 19 | package singleflight 20 | 21 | import ( 22 | "fmt" 23 | "sync" 24 | ) 25 | 26 | // call is an in-flight or completed Do call 27 | type call struct { 28 | wg sync.WaitGroup 29 | val interface{} 30 | err error 31 | } 32 | 33 | // Group represents a class of work and forms a namespace in which 34 | // units of work can be executed with duplicate suppression. 35 | type Group struct { 36 | mu sync.Mutex // protects m 37 | m map[string]*call // lazily initialized 38 | } 39 | 40 | // Do executes and returns the results of the given function, making 41 | // sure that only one execution is in-flight for a given key at a 42 | // time. If a duplicate comes in, the duplicate caller waits for the 43 | // original to complete and receives the same results. 44 | func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { 45 | g.mu.Lock() 46 | if g.m == nil { 47 | g.m = make(map[string]*call) 48 | } 49 | if c, ok := g.m[key]; ok { 50 | g.mu.Unlock() 51 | c.wg.Wait() 52 | return c.val, c.err 53 | } 54 | c := &call{ 55 | err: fmt.Errorf("singleflight leader panicked"), 56 | } 57 | c.wg.Add(1) 58 | g.m[key] = c 59 | g.mu.Unlock() 60 | 61 | defer func() { 62 | c.wg.Done() 63 | 64 | g.mu.Lock() 65 | delete(g.m, key) 66 | g.mu.Unlock() 67 | }() 68 | 69 | c.val, c.err = fn() 70 | 71 | return c.val, c.err 72 | } 73 | 74 | // Lock prevents single flights from occurring for the duration 75 | // of the provided function. This allows users to clear caches 76 | // or preform some operation in between running flights. 77 | func (g *Group) Lock(fn func()) { 78 | g.mu.Lock() 79 | defer g.mu.Unlock() 80 | fn() 81 | } 82 | -------------------------------------------------------------------------------- /transport/tracer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | Copyright Arsene Tochemey Gandote 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package transport 19 | 20 | import ( 21 | "sync" 22 | 23 | "go.opentelemetry.io/otel" 24 | "go.opentelemetry.io/otel/attribute" 25 | "go.opentelemetry.io/otel/trace" 26 | ) 27 | 28 | const ( 29 | instrumentationName = "github.com/groupcache/groupcache-go/instrumentation/otel" 30 | ) 31 | 32 | type TracerOption func(*Tracer) 33 | 34 | type Tracer struct { 35 | traceProvider trace.TracerProvider 36 | tracer trace.Tracer 37 | traceAttributes []attribute.KeyValue 38 | spanStartOptionsPool sync.Pool 39 | attributesPool sync.Pool 40 | } 41 | 42 | func WithTraceProvider(tp trace.TracerProvider) TracerOption { 43 | return func(t *Tracer) { 44 | if tp != nil { 45 | t.traceProvider = tp 46 | } 47 | } 48 | } 49 | 50 | func WithTracerAttributes(attrs ...attribute.KeyValue) TracerOption { 51 | return func(t *Tracer) { 52 | t.traceAttributes = append(t.traceAttributes, attrs...) 53 | } 54 | } 55 | 56 | func NewTracer(opts ...TracerOption) *Tracer { 57 | t := &Tracer{ 58 | traceProvider: otel.GetTracerProvider(), 59 | spanStartOptionsPool: sync.Pool{ 60 | New: func() any { 61 | s := make([]trace.SpanStartOption, 0, 10) 62 | return &s 63 | }, 64 | }, 65 | attributesPool: sync.Pool{ 66 | New: func() any { 67 | s := make([]attribute.KeyValue, 0, 10) 68 | return &s 69 | }, 70 | }, 71 | } 72 | 73 | for _, opt := range opts { 74 | opt(t) 75 | } 76 | 77 | t.tracer = t.traceProvider.Tracer(instrumentationName) 78 | return t 79 | } 80 | 81 | func (t *Tracer) getTracer() trace.Tracer { 82 | return t.tracer 83 | } 84 | 85 | func (t *Tracer) getSpanStartOptions() *[]trace.SpanStartOption { 86 | return t.spanStartOptionsPool.Get().(*[]trace.SpanStartOption) 87 | } 88 | 89 | func (t *Tracer) putSpanStartOptions(opts *[]trace.SpanStartOption) { 90 | *opts = (*opts)[:0] 91 | t.spanStartOptionsPool.Put(opts) 92 | } 93 | 94 | func (t *Tracer) getAttributes() *[]attribute.KeyValue { 95 | return t.attributesPool.Get().(*[]attribute.KeyValue) 96 | } 97 | 98 | func (t *Tracer) putAttributes(attrs *[]attribute.KeyValue) { 99 | *attrs = (*attrs)[:0] 100 | t.attributesPool.Put(attrs) 101 | } 102 | -------------------------------------------------------------------------------- /contrib/otter_test.go: -------------------------------------------------------------------------------- 1 | package contrib_test 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | "time" 7 | 8 | "github.com/groupcache/groupcache-go/v3/contrib" 9 | "github.com/groupcache/groupcache-go/v3/transport" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestOtterCrud(t *testing.T) { 15 | c, err := contrib.NewOtterCache(20_000) 16 | require.NoError(t, err) 17 | 18 | c.Add("key1", transport.ByteViewWithExpire([]byte("value1"), time.Time{})) 19 | 20 | v, ok := c.Get("key1") 21 | assert.True(t, ok) 22 | assert.Equal(t, "value1", v.String()) 23 | assert.Equal(t, int64(1), c.Stats().Hits) 24 | assert.Equal(t, int64(1), c.Stats().Gets) 25 | assert.Equal(t, int64(1), c.Stats().Items) 26 | 27 | // This item should be rejected by otter as it's "cost" is too high 28 | c.Add("too-large", transport.ByteViewWithExpire(randomValue((20_000/10)+1), time.Time{})) 29 | assert.Equal(t, int64(1), c.Stats().Rejected) 30 | assert.Equal(t, int64(1), c.Stats().Items) 31 | 32 | c.Remove("key1") 33 | assert.Equal(t, int64(1), c.Stats().Hits) 34 | assert.Equal(t, int64(1), c.Stats().Gets) 35 | assert.Equal(t, int64(0), c.Stats().Items) 36 | } 37 | 38 | func TestOtterEnsureUpdateExpiredValue(t *testing.T) { 39 | c, err := contrib.NewOtterCache(20_000) 40 | require.NoError(t, err) 41 | curTime := time.Now() 42 | 43 | // Override the now function so we control time 44 | c.Now = func() time.Time { 45 | return curTime 46 | } 47 | 48 | // Expires in 1 second 49 | c.Add("key1", transport.ByteViewWithExpire([]byte("value1"), curTime.Add(time.Second))) 50 | _, ok := c.Get("key1") 51 | assert.True(t, ok) 52 | 53 | // Advance 1.1 seconds into the future 54 | curTime = curTime.Add(time.Millisecond * 1100) 55 | 56 | // Value should have expired 57 | _, ok = c.Get("key1") 58 | assert.False(t, ok) 59 | 60 | // Add a new key that expires in 1 second 61 | c.Add("key2", transport.ByteViewWithExpire([]byte("value2"), curTime.Add(time.Second))) 62 | _, ok = c.Get("key2") 63 | assert.True(t, ok) 64 | 65 | // Advance 0.5 seconds into the future 66 | curTime = curTime.Add(time.Millisecond * 500) 67 | 68 | // Value should still exist 69 | _, ok = c.Get("key2") 70 | assert.True(t, ok) 71 | 72 | // Replace the existing key, this should update the expired time 73 | c.Add("key2", transport.ByteViewWithExpire([]byte("updated value2"), curTime.Add(time.Second))) 74 | _, ok = c.Get("key2") 75 | assert.True(t, ok) 76 | 77 | // Advance 0.6 seconds into the future, which puts us past the initial 78 | // expired time for key2. 79 | curTime = curTime.Add(time.Millisecond * 600) 80 | 81 | // Should still exist 82 | _, ok = c.Get("key2") 83 | assert.True(t, ok) 84 | 85 | // Advance 1.1 seconds into the future 86 | curTime = curTime.Add(time.Millisecond * 1100) 87 | 88 | // Should not exist 89 | _, ok = c.Get("key2") 90 | assert.False(t, ok) 91 | } 92 | 93 | func randomValue(length int) []byte { 94 | bytes := make([]byte, length) 95 | _, _ = rand.Read(bytes) 96 | return bytes 97 | } 98 | -------------------------------------------------------------------------------- /transport/tls_test.go: -------------------------------------------------------------------------------- 1 | package transport_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/groupcache/groupcache-go/v3" 7 | "github.com/groupcache/groupcache-go/v3/cluster" 8 | "github.com/groupcache/groupcache-go/v3/transport" 9 | "github.com/groupcache/groupcache-go/v3/transport/pb" 10 | "github.com/kapetan-io/tackle/autotls" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "io" 14 | "log/slog" 15 | "net/http" 16 | "net/http/httptest" 17 | "strconv" 18 | "strings" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestTLS(t *testing.T) { 24 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 25 | defer cancel() 26 | 27 | // AutoGenerate TLS certs 28 | conf := autotls.Config{AutoTLS: true} 29 | require.NoError(t, autotls.Setup(&conf)) 30 | 31 | // Start a 2 node cluster with TLS 32 | err := cluster.Start(context.Background(), 2, 33 | groupcache.Options{ 34 | Transport: transport.NewHttpTransport(transport.HttpTransportOptions{ 35 | TLSConfig: conf.ServerTLS, 36 | Client: &http.Client{ 37 | Transport: &http.Transport{ 38 | TLSClientConfig: conf.ClientTLS, 39 | }, 40 | }, 41 | }), 42 | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 43 | }) 44 | require.NoError(t, err) 45 | 46 | assert.Equal(t, 2, len(cluster.ListPeers())) 47 | assert.Equal(t, 2, len(cluster.ListDaemons())) 48 | 49 | // Start a http server to count the number of non cached hits 50 | var serverHits int 51 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | _, _ = fmt.Fprintln(w, "Hello") 53 | serverHits++ 54 | })) 55 | defer ts.Close() 56 | 57 | // Create a group on each instance in the cluster 58 | for idx, d := range cluster.ListDaemons() { 59 | _, err := d.GetInstance().NewGroup(groupName, 1<<20, 60 | groupcache.GetterFunc(func(ctx context.Context, key string, dest transport.Sink) error { 61 | if _, err := http.Get(ts.URL); err != nil { 62 | t.Logf("HTTP request from getter failed with '%s'", err) 63 | } 64 | return dest.SetString(strconv.Itoa(idx)+":"+key, time.Time{}) 65 | })) 66 | require.NoError(t, err) 67 | } 68 | 69 | // Create new transport with the client TLS config 70 | tr := transport.NewHttpTransport(transport.HttpTransportOptions{ 71 | TLSConfig: conf.ServerTLS, 72 | Client: &http.Client{ 73 | Transport: &http.Transport{ 74 | TLSClientConfig: conf.ClientTLS, 75 | }, 76 | }, 77 | }) 78 | 79 | // Create a new client to the first peer in the cluster 80 | c, err := tr.NewClient(ctx, cluster.PeerAt(0)) 81 | require.NoError(t, err) 82 | 83 | // Each new key should result in a new hit to the test server 84 | for _, key := range testKeys(100) { 85 | var resp pb.GetResponse 86 | require.NoError(t, getRequest(ctx, c, groupName, key, &resp)) 87 | 88 | // The value should be in the format `instance:key` 89 | assert.True(t, strings.HasSuffix(string(resp.Value), ":"+key)) 90 | } 91 | assert.Equal(t, 100, serverHits) 92 | 93 | require.NoError(t, cluster.Shutdown(context.Background())) 94 | } 95 | -------------------------------------------------------------------------------- /cluster/cluster_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cluster_test 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/groupcache/groupcache-go/v3" 24 | "github.com/groupcache/groupcache-go/v3/cluster" 25 | "github.com/groupcache/groupcache-go/v3/transport/peer" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestStartMultipleInstances(t *testing.T) { 31 | err := cluster.Start(context.Background(), 2, groupcache.Options{}) 32 | require.NoError(t, err) 33 | 34 | assert.Equal(t, 2, len(cluster.ListPeers())) 35 | assert.Equal(t, 2, len(cluster.ListDaemons())) 36 | require.NoError(t, cluster.Shutdown(context.Background())) 37 | } 38 | 39 | func TestRestart(t *testing.T) { 40 | err := cluster.Start(context.Background(), 2, groupcache.Options{}) 41 | require.NoError(t, err) 42 | 43 | assert.Equal(t, 2, len(cluster.ListPeers())) 44 | assert.Equal(t, 2, len(cluster.ListDaemons())) 45 | err = cluster.Restart(context.Background()) 46 | require.NoError(t, err) 47 | require.NoError(t, cluster.Shutdown(context.Background())) 48 | } 49 | 50 | func TestStartOneInstance(t *testing.T) { 51 | err := cluster.Start(context.Background(), 1, groupcache.Options{}) 52 | require.NoError(t, err) 53 | 54 | assert.Equal(t, 1, len(cluster.ListPeers())) 55 | assert.Equal(t, 1, len(cluster.ListDaemons())) 56 | require.NoError(t, cluster.Shutdown(context.Background())) 57 | } 58 | 59 | func TestStartMultipleDaemons(t *testing.T) { 60 | peers := []peer.Info{ 61 | {Address: "localhost:1111"}, 62 | {Address: "localhost:2222"}} 63 | err := cluster.StartWith(context.Background(), peers, groupcache.Options{}) 64 | require.NoError(t, err) 65 | 66 | daemons := cluster.ListDaemons() 67 | assert.Equal(t, 2, len(daemons)) 68 | // If local system uses IPV6 localhost will resolve to ::1 if IPV4 then it will be 127.0.0.1, 69 | // so we only compare the ports and assume the local part resolved correctly depending on the system. 70 | assert.Contains(t, daemons[0].ListenAddress(), ":1111") 71 | assert.Contains(t, daemons[1].ListenAddress(), ":2222") 72 | assert.Contains(t, cluster.DaemonAt(0).ListenAddress(), ":1111") 73 | assert.Contains(t, cluster.DaemonAt(1).ListenAddress(), ":2222") 74 | require.NoError(t, cluster.Shutdown(context.Background())) 75 | } 76 | 77 | func TestStartWithInvalidPeer(t *testing.T) { 78 | err := cluster.StartWith(context.Background(), []peer.Info{{Address: "1111"}}, groupcache.Options{}) 79 | assert.Error(t, err) 80 | assert.Nil(t, cluster.ListPeers()) 81 | assert.Nil(t, cluster.ListDaemons()) 82 | require.NoError(t, cluster.Shutdown(context.Background())) 83 | } 84 | -------------------------------------------------------------------------------- /contrib/otter.go: -------------------------------------------------------------------------------- 1 | package contrib 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | 7 | "github.com/groupcache/groupcache-go/v3" 8 | "github.com/groupcache/groupcache-go/v3/transport" 9 | "github.com/maypok86/otter" 10 | ) 11 | 12 | type NowFunc func() time.Time 13 | 14 | // OtterCache is an alternative cache implementation which uses a high performance lockless 15 | // cache suitable for use in high concurrency environments where mutex contention is an issue. 16 | type OtterCache struct { 17 | cache otter.Cache[string, transport.ByteView] 18 | rejected atomic.Int64 19 | gets atomic.Int64 20 | 21 | // Now is the Now() function the cache will use to determine 22 | // the current time which is used to calculate expired values 23 | // Defaults to time.Now() 24 | Now NowFunc 25 | } 26 | 27 | // NewOtterCache instantiates a new cache instance 28 | func NewOtterCache(maxBytes int64) (*OtterCache, error) { 29 | o := &OtterCache{ 30 | Now: time.Now, 31 | } 32 | 33 | var err error 34 | o.cache, err = otter.MustBuilder[string, transport.ByteView](int(maxBytes)). 35 | CollectStats(). 36 | Cost(func(key string, value transport.ByteView) uint32 { 37 | return uint32(value.Len()) 38 | }). 39 | Build() 40 | return o, err 41 | } 42 | 43 | // Get returns the item from the cache 44 | func (o *OtterCache) Get(key string) (transport.ByteView, bool) { 45 | i, ok := o.cache.Get(key) 46 | 47 | // We don't use otter's TTL as it is universal to every item 48 | // in the cache and groupcache allows users to set a TTL per 49 | // item stored. 50 | if !i.Expire().IsZero() && i.Expire().Before(o.Now()) { 51 | o.cache.Delete(key) 52 | return transport.ByteView{}, false 53 | } 54 | o.gets.Add(1) 55 | return i, ok 56 | } 57 | 58 | // Add adds the item to the cache. However, otter has the side effect 59 | // of rejecting an item if the items size (aka, cost) is larger than 60 | // the capacity (max cost) of the cache divided by 10. 61 | // 62 | // If Stats() reports a high number of Rejected items due to large 63 | // cached items exceeding the maximum cost of the "Hot Cache", then you 64 | // should increase the size of the cache such that no cache item is 65 | // larger than the total size of the cache divided by 10. 66 | // 67 | // See s3fifo/policy.go NewPolicy() for details 68 | func (o *OtterCache) Add(key string, value transport.ByteView) { 69 | if ok := o.cache.Set(key, value); !ok { 70 | o.rejected.Add(1) 71 | } 72 | } 73 | 74 | func (o *OtterCache) Remove(key string) { 75 | o.cache.Delete(key) 76 | } 77 | 78 | func (o *OtterCache) Stats() groupcache.CacheStats { 79 | s := o.cache.Stats() 80 | return groupcache.CacheStats{ 81 | Bytes: int64(o.cache.Capacity()), 82 | Items: int64(o.cache.Size()), 83 | Rejected: o.rejected.Load(), 84 | Evictions: s.EvictedCount(), 85 | Gets: o.gets.Load(), 86 | Hits: s.Hits(), 87 | } 88 | } 89 | 90 | // Bytes always returns 0 bytes used. Otter does not keep track of total bytes, 91 | // and it is impractical for us to attempt to keep track of total bytes in the 92 | // cache. Tracking the size of Add and Eviction is easy. However, we must also 93 | // adjust the total bytes count when items with the same key are replaced. 94 | // Doing so is more computationally expensive as we must check the cache for an 95 | // existing item, subtract the existing byte count, then add the new byte count 96 | // of the replacing item. 97 | // 98 | // Arguably reporting the total bytes used is not as useful as hit ratio 99 | // in a production environment. 100 | func (o *OtterCache) Bytes() int64 { 101 | return 0 102 | } 103 | 104 | func (o *OtterCache) Close() { 105 | o.cache.Close() 106 | } 107 | -------------------------------------------------------------------------------- /daemon.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package groupcache 18 | 19 | import ( 20 | "context" 21 | "log/slog" 22 | 23 | "github.com/groupcache/groupcache-go/v3/transport" 24 | "github.com/groupcache/groupcache-go/v3/transport/peer" 25 | ) 26 | 27 | // Daemon is an instance of groupcache bound to a port listening for requests 28 | type Daemon struct { 29 | instance *Instance 30 | opts Options 31 | address string 32 | } 33 | 34 | // ListenAndServe creates a new instance of groupcache listening on the address provided 35 | func ListenAndServe(ctx context.Context, address string, opts Options) (*Daemon, error) { 36 | if opts.Logger == nil { 37 | opts.Logger = slog.Default() 38 | } 39 | 40 | if opts.Transport == nil { 41 | opts.Transport = transport.NewHttpTransport(transport.HttpTransportOptions{}) 42 | } 43 | 44 | daemon := &Daemon{ 45 | address: address, 46 | opts: opts, 47 | } 48 | 49 | return daemon, daemon.Start(ctx) 50 | } 51 | 52 | func (d *Daemon) Start(ctx context.Context) error { 53 | d.instance = New(d.opts) 54 | return d.opts.Transport.ListenAndServe(ctx, d.address) 55 | } 56 | 57 | // NewGroup is a convenience method which calls NewGroup on the instance associated with this daemon. 58 | func (d *Daemon) NewGroup(name string, cacheBytes int64, getter Getter) (transport.Group, error) { 59 | return d.instance.NewGroup(name, cacheBytes, getter) 60 | } 61 | 62 | // GetGroup is a convenience method which calls GetGroup on the instance associated with this daemon 63 | func (d *Daemon) GetGroup(name string) transport.Group { 64 | return d.instance.GetGroup(name) 65 | } 66 | 67 | // RemoveGroup is a convenience method which calls RemoveGroup on the instance associated with this daemon 68 | func (d *Daemon) RemoveGroup(name string) { 69 | d.instance.RemoveGroup(name) 70 | } 71 | 72 | // GetInstance returns the current groupcache instance associated with this daemon 73 | func (d *Daemon) GetInstance() *Instance { 74 | return d.instance 75 | } 76 | 77 | // SetPeers is a convenience method which calls SetPeers on the instance associated with this daemon. In 78 | // addition, it finds and marks this instance as self by asking the transport for it's listening address 79 | // before calling SetPeers() on the instance. If this is not desirable, call Daemon.GetInstance().SetPeers() 80 | // instead. 81 | func (d *Daemon) SetPeers(ctx context.Context, src []peer.Info) error { 82 | dest := make([]peer.Info, len(src)) 83 | for idx := 0; idx < len(src); idx++ { 84 | dest[idx] = src[idx] 85 | if dest[idx].Address == d.ListenAddress() { 86 | dest[idx].IsSelf = true 87 | } 88 | } 89 | return d.instance.SetPeers(ctx, dest) 90 | } 91 | 92 | // MustClient is a convenience method which creates a new client for this instance. This method will 93 | // panic if transport.NewClient() returns an error. 94 | func (d *Daemon) MustClient() peer.Client { 95 | c, err := d.opts.Transport.NewClient(context.Background(), peer.Info{Address: d.ListenAddress()}) 96 | if err != nil { 97 | panic(err) 98 | } 99 | return c 100 | } 101 | 102 | // ListenAddress returns the address this instance is listening on 103 | func (d *Daemon) ListenAddress() string { 104 | return d.opts.Transport.ListenAddress() 105 | } 106 | 107 | // Shutdown attempts a clean shutdown of the daemon and all related resources. 108 | func (d *Daemon) Shutdown(ctx context.Context) error { 109 | return d.opts.Transport.Shutdown(ctx) 110 | } 111 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // main package is intended for testing the interaction between instances. Calls to `/set` 18 | // will ONLY set the value for the running instance and NOT the groupcache. Values are stored 19 | // in a global `store` and NOT in the groupcache group until they are fetched by a call to `/cache`. 20 | package main 21 | 22 | import ( 23 | "context" 24 | "flag" 25 | "fmt" 26 | "log" 27 | "log/slog" 28 | "net/http" 29 | "os" 30 | "os/signal" 31 | "strings" 32 | "syscall" 33 | "time" 34 | 35 | "github.com/groupcache/groupcache-go/v3" 36 | "github.com/groupcache/groupcache-go/v3/transport" 37 | "github.com/groupcache/groupcache-go/v3/transport/peer" 38 | ) 39 | 40 | var store = map[string]string{} 41 | 42 | func main() { 43 | addrFlag := flag.String("addr", "localhost:8080", "address:port to bind to") 44 | peerFlag := flag.String("peers", "localhost:8080", "a comma-separated list of groupcache peers") 45 | flag.Parse() 46 | 47 | var peers []peer.Info 48 | for _, p := range strings.Split(*peerFlag, ",") { 49 | peers = append(peers, peer.Info{ 50 | IsSelf: p == *addrFlag, 51 | Address: p, 52 | }) 53 | } 54 | 55 | t := transport.NewHttpTransport(transport.HttpTransportOptions{}) 56 | i := groupcache.New(groupcache.Options{ 57 | Logger: slog.Default(), 58 | Transport: t, 59 | }) 60 | 61 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 62 | defer cancel() 63 | err := i.SetPeers(ctx, peers) 64 | if err != nil { 65 | log.Fatal(err.Error()) 66 | } 67 | 68 | group, err := i.NewGroup("cache1", 64<<20, groupcache.GetterFunc( 69 | func(ctx context.Context, key string, dest transport.Sink) error { 70 | v, ok := store[key] 71 | if !ok { 72 | return fmt.Errorf("key not set") 73 | } else { 74 | if err := dest.SetBytes([]byte(v), time.Now().Add(10*time.Minute)); err != nil { 75 | log.Printf("Failed to set cache value for key '%s' - %v\n", key, err) 76 | return err 77 | } 78 | } 79 | 80 | return nil 81 | }, 82 | )) 83 | if err != nil { 84 | log.Fatal(err.Error()) 85 | } 86 | 87 | mux := http.NewServeMux() 88 | mux.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 89 | _ = r.ParseForm() 90 | key := r.FormValue("key") 91 | value := r.FormValue("value") 92 | fmt.Printf("Set: [%s]%s\n", key, value) 93 | store[key] = value 94 | }) 95 | 96 | mux.HandleFunc("/cache", func(w http.ResponseWriter, r *http.Request) { 97 | key := r.FormValue("key") 98 | 99 | fmt.Printf("Fetching value for key '%s'\n", key) 100 | 101 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 102 | defer cancel() 103 | 104 | var b []byte 105 | err := group.Get(ctx, key, transport.AllocatingByteSliceSink(&b)) 106 | if err != nil { 107 | http.Error(w, err.Error(), http.StatusNotFound) 108 | return 109 | } 110 | _, _ = w.Write(b) 111 | _, _ = w.Write([]byte{'\n'}) 112 | }) 113 | mux.Handle(transport.DefaultBasePath, t) 114 | 115 | server := http.Server{ 116 | Addr: *addrFlag, 117 | Handler: mux, 118 | } 119 | 120 | go func() { 121 | log.Printf("Listening on '%s' for API requests", *addrFlag) 122 | if err := server.ListenAndServe(); err != nil { 123 | log.Fatalf("Failed to start HTTP server - %v", err) 124 | } 125 | }() 126 | 127 | termChan := make(chan os.Signal, 1) 128 | signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM) 129 | <-termChan 130 | } 131 | -------------------------------------------------------------------------------- /internal/singleflight/singleflight_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package singleflight 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "strings" 23 | "sync" 24 | "sync/atomic" 25 | "testing" 26 | "time" 27 | ) 28 | 29 | func TestDo(t *testing.T) { 30 | var g Group 31 | v, err := g.Do("key", func() (interface{}, error) { 32 | return "bar", nil 33 | }) 34 | if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { 35 | t.Errorf("Do = %v; want %v", got, want) 36 | } 37 | if err != nil { 38 | t.Errorf("Do error = %v", err) 39 | } 40 | } 41 | 42 | func TestDoErr(t *testing.T) { 43 | var g Group 44 | someErr := errors.New("Some error") 45 | v, err := g.Do("key", func() (interface{}, error) { 46 | return nil, someErr 47 | }) 48 | if err != someErr { 49 | t.Errorf("Do error = %v; want someErr", err) 50 | } 51 | if v != nil { 52 | t.Errorf("unexpected non-nil value %#v", v) 53 | } 54 | } 55 | 56 | func TestDoDupSuppress(t *testing.T) { 57 | var g Group 58 | c := make(chan string) 59 | var calls int32 60 | fn := func() (interface{}, error) { 61 | atomic.AddInt32(&calls, 1) 62 | return <-c, nil 63 | } 64 | 65 | const n = 10 66 | var wg sync.WaitGroup 67 | for i := 0; i < n; i++ { 68 | wg.Add(1) 69 | go func() { 70 | v, err := g.Do("key", fn) 71 | if err != nil { 72 | t.Errorf("Do error: %v", err) 73 | } 74 | if v.(string) != "bar" { 75 | t.Errorf("got %q; want %q", v, "bar") 76 | } 77 | wg.Done() 78 | }() 79 | } 80 | time.Sleep(100 * time.Millisecond) // let goroutines above block 81 | c <- "bar" 82 | wg.Wait() 83 | if got := atomic.LoadInt32(&calls); got != 1 { 84 | t.Errorf("number of calls = %d; want 1", got) 85 | } 86 | } 87 | 88 | func TestDoPanic(t *testing.T) { 89 | var g Group 90 | var err error 91 | func() { 92 | defer func() { 93 | // do not let the panic below leak to the test 94 | _ = recover() 95 | }() 96 | _, err = g.Do("key", func() (interface{}, error) { 97 | panic("something went horribly wrong") 98 | }) 99 | }() 100 | if err != nil { 101 | t.Errorf("Do error = %v; want someErr", err) 102 | } 103 | // ensure subsequent calls to same key still work 104 | v, err := g.Do("key", func() (interface{}, error) { 105 | return "foo", nil 106 | }) 107 | if err != nil { 108 | t.Errorf("Do error = %v; want no error", err) 109 | } 110 | if v.(string) != "foo" { 111 | t.Errorf("got %q; want %q", v, "foo") 112 | } 113 | } 114 | 115 | func TestDoConcurrentPanic(t *testing.T) { 116 | var g Group 117 | c := make(chan struct{}) 118 | var calls int32 119 | fn := func() (interface{}, error) { 120 | atomic.AddInt32(&calls, 1) 121 | <-c 122 | panic("something went horribly wrong") 123 | } 124 | 125 | const n = 10 126 | var wg sync.WaitGroup 127 | for i := 0; i < n; i++ { 128 | wg.Add(1) 129 | go func() { 130 | defer func() { 131 | // do not let the panic leak to the test 132 | _ = recover() 133 | wg.Done() 134 | }() 135 | 136 | v, err := g.Do("key", fn) 137 | if err == nil || !strings.Contains(err.Error(), "singleflight leader panicked") { 138 | t.Errorf("Do error: %v; wanted 'singleflight panicked'", err) 139 | } 140 | if v != nil { 141 | t.Errorf("got %q; want nil", v) 142 | } 143 | }() 144 | } 145 | time.Sleep(100 * time.Millisecond) // let goroutines above block 146 | c <- struct{}{} 147 | wg.Wait() 148 | if got := atomic.LoadInt32(&calls); got != 1 { 149 | t.Errorf("number of calls = %d; want 1", got) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /transport/tracer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | Copyright Arsene Tochemey Gandote 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package transport 19 | 20 | import ( 21 | "sync" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | "go.opentelemetry.io/otel" 26 | "go.opentelemetry.io/otel/attribute" 27 | "go.opentelemetry.io/otel/trace" 28 | "go.opentelemetry.io/otel/trace/noop" 29 | ) 30 | 31 | type recorderTracerProvider struct { 32 | trace.TracerProvider 33 | mu sync.Mutex 34 | requested []string 35 | } 36 | 37 | func (r *recorderTracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer { 38 | r.mu.Lock() 39 | r.requested = append(r.requested, name) 40 | r.mu.Unlock() 41 | return r.TracerProvider.Tracer(name, opts...) 42 | } 43 | 44 | func newRecorderTracerProvider() *recorderTracerProvider { 45 | return &recorderTracerProvider{TracerProvider: noop.NewTracerProvider()} 46 | } 47 | 48 | // nolint 49 | func TestNewTracerUsesGlobalProviderWhenNoneProvided(t *testing.T) { 50 | t.Parallel() 51 | 52 | original := otel.GetTracerProvider() 53 | rec := newRecorderTracerProvider() 54 | otel.SetTracerProvider(rec) 55 | defer otel.SetTracerProvider(original) 56 | 57 | tracer := NewTracer() 58 | 59 | require.Equal(t, rec, tracer.traceProvider) 60 | require.Equal(t, []string{instrumentationName}, rec.requested) 61 | require.NotNil(t, tracer.getTracer()) 62 | } 63 | 64 | // nolint 65 | func TestNewTracerRespectsCustomProviderOption(t *testing.T) { 66 | t.Parallel() 67 | 68 | original := otel.GetTracerProvider() 69 | defer otel.SetTracerProvider(original) 70 | 71 | // Set a global provider that should not be used after the override. 72 | global := newRecorderTracerProvider() 73 | otel.SetTracerProvider(global) 74 | 75 | custom := newRecorderTracerProvider() 76 | tracer := NewTracer(WithTraceProvider(custom)) 77 | 78 | require.Equal(t, custom, tracer.traceProvider) 79 | require.Equal(t, []string{instrumentationName}, custom.requested) 80 | require.Empty(t, global.requested) 81 | } 82 | 83 | // nolint 84 | func TestWithTracerAttributesAppendsAttributes(t *testing.T) { 85 | t.Parallel() 86 | 87 | attrs := []attribute.KeyValue{ 88 | attribute.String("env", "test"), 89 | attribute.Int("shard", 1), 90 | } 91 | 92 | tracer := NewTracer(WithTracerAttributes(attrs...)) 93 | require.Equal(t, attrs, tracer.traceAttributes) 94 | } 95 | 96 | // nolint 97 | func TestSpanStartOptionsPoolResetsOnPut(t *testing.T) { 98 | t.Parallel() 99 | 100 | tracer := NewTracer() 101 | 102 | opts := tracer.getSpanStartOptions() 103 | require.Zero(t, len(*opts)) 104 | require.GreaterOrEqual(t, cap(*opts), 10) 105 | 106 | *opts = append(*opts, trace.WithSpanKind(trace.SpanKindClient)) 107 | require.Equal(t, 1, len(*opts)) 108 | 109 | tracer.putSpanStartOptions(opts) 110 | 111 | opts = tracer.getSpanStartOptions() 112 | require.Zero(t, len(*opts)) 113 | require.GreaterOrEqual(t, cap(*opts), 10) 114 | tracer.putSpanStartOptions(opts) 115 | } 116 | 117 | // nolint 118 | func TestAttributesPoolResetsOnPut(t *testing.T) { 119 | t.Parallel() 120 | 121 | tracer := NewTracer() 122 | 123 | attrs := tracer.getAttributes() 124 | require.Zero(t, len(*attrs)) 125 | require.GreaterOrEqual(t, cap(*attrs), 10) 126 | 127 | *attrs = append(*attrs, attribute.String("key", "val")) 128 | require.Equal(t, 1, len(*attrs)) 129 | 130 | tracer.putAttributes(attrs) 131 | 132 | attrs = tracer.getAttributes() 133 | require.Zero(t, len(*attrs)) 134 | require.GreaterOrEqual(t, cap(*attrs), 10) 135 | tracer.putAttributes(attrs) 136 | } 137 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright Derrick J Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package groupcache 19 | 20 | import ( 21 | "testing" 22 | "time" 23 | 24 | "github.com/groupcache/groupcache-go/v3/transport" 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestEnsureSizeReportedCorrectly(t *testing.T) { 29 | c := newMutexCache(0) 30 | 31 | // Add the first value 32 | bv1 := transport.ByteViewWithExpire([]byte("first"), time.Now().Add(100*time.Second)) 33 | c.Add("key1", bv1) 34 | v, ok := c.Get("key1") 35 | 36 | // Should be len("key1" + "first") == 9 37 | assert.True(t, ok) 38 | assert.True(t, v.Equal(bv1)) 39 | assert.Equal(t, int64(9), c.Bytes()) 40 | 41 | // Add a second value 42 | bv2 := transport.ByteViewWithExpire([]byte("second"), time.Now().Add(200*time.Second)) 43 | 44 | c.Add("key2", bv2) 45 | v, ok = c.Get("key2") 46 | 47 | // Should be len("key2" + "second") == (10 + 9) == 19 48 | assert.True(t, ok) 49 | assert.True(t, v.Equal(bv2)) 50 | assert.Equal(t, int64(19), c.Bytes()) 51 | 52 | // Replace the first value with a shorter value 53 | bv3 := transport.ByteViewWithExpire([]byte("3"), time.Now().Add(200*time.Second)) 54 | 55 | c.Add("key1", bv3) 56 | v, ok = c.Get("key1") 57 | 58 | // len("key1" + "3") == 5 59 | // len("key2" + "second") == 10 60 | assert.True(t, ok) 61 | assert.True(t, v.Equal(bv3)) 62 | assert.Equal(t, int64(15), c.Bytes()) 63 | 64 | // Replace the second value with a longer value 65 | bv4 := transport.ByteViewWithExpire([]byte("this-string-is-28-chars-long"), time.Now().Add(200*time.Second)) 66 | 67 | c.Add("key2", bv4) 68 | v, ok = c.Get("key2") 69 | 70 | // len("key1" + "3") == 5 71 | // len("key2" + "this-string-is-28-chars-long") == 32 72 | assert.True(t, ok) 73 | assert.True(t, v.Equal(bv4)) 74 | assert.Equal(t, int64(37), c.Bytes()) 75 | } 76 | 77 | func TestEnsureUpdateExpiredValue(t *testing.T) { 78 | c := newMutexCache(20_000) 79 | curTime := time.Now() 80 | 81 | // Override the now function so we control time 82 | nowFunc = func() time.Time { 83 | return curTime 84 | } 85 | defer func() { 86 | nowFunc = time.Now 87 | }() 88 | 89 | // Expires in 1 second 90 | c.Add("key1", transport.ByteViewWithExpire([]byte("value1"), curTime.Add(time.Second))) 91 | _, ok := c.Get("key1") 92 | assert.True(t, ok) 93 | 94 | // Advance 1.1 seconds into the future 95 | curTime = curTime.Add(time.Millisecond * 1100) 96 | 97 | // Value should have expired 98 | _, ok = c.Get("key1") 99 | assert.False(t, ok) 100 | 101 | // Add a new key that expires in 1 second 102 | c.Add("key2", transport.ByteViewWithExpire([]byte("value2"), curTime.Add(time.Second))) 103 | _, ok = c.Get("key2") 104 | assert.True(t, ok) 105 | 106 | // Advance 0.5 seconds into the future 107 | curTime = curTime.Add(time.Millisecond * 500) 108 | 109 | // Value should still exist 110 | _, ok = c.Get("key2") 111 | assert.True(t, ok) 112 | 113 | // Replace the existing key, this should update the expired time 114 | c.Add("key2", transport.ByteViewWithExpire([]byte("updated value2"), curTime.Add(time.Second))) 115 | _, ok = c.Get("key2") 116 | assert.True(t, ok) 117 | 118 | // Advance 0.6 seconds into the future, which puts us past the initial 119 | // expired time for key2. 120 | curTime = curTime.Add(time.Millisecond * 600) 121 | 122 | // Should still exist 123 | _, ok = c.Get("key2") 124 | assert.True(t, ok) 125 | 126 | // Advance 1.1 seconds into the future 127 | curTime = curTime.Add(time.Millisecond * 1100) 128 | 129 | // Should not exist 130 | _, ok = c.Get("key2") 131 | assert.False(t, ok) 132 | } 133 | -------------------------------------------------------------------------------- /transport/byteview_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright 2024 Derrick J Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package transport_test 19 | 20 | import ( 21 | "bytes" 22 | "fmt" 23 | "io" 24 | "testing" 25 | 26 | "github.com/groupcache/groupcache-go/v3/transport" 27 | ) 28 | 29 | func TestByteView(t *testing.T) { 30 | for _, s := range []string{"", "x", "yy"} { 31 | for _, v := range []transport.ByteView{transport.ByteViewFrom([]byte(s)), transport.ByteViewFrom(s)} { 32 | name := fmt.Sprintf("string %q, view %+v", s, v) 33 | if v.Len() != len(s) { 34 | t.Errorf("%s: Len = %d; want %d", name, v.Len(), len(s)) 35 | } 36 | if v.String() != s { 37 | t.Errorf("%s: String = %q; want %q", name, v.String(), s) 38 | } 39 | var longDest [3]byte 40 | if n := v.Copy(longDest[:]); n != len(s) { 41 | t.Errorf("%s: long Copy = %d; want %d", name, n, len(s)) 42 | } 43 | var shortDest [1]byte 44 | if n := v.Copy(shortDest[:]); n != min(len(s), 1) { 45 | t.Errorf("%s: short Copy = %d; want %d", name, n, min(len(s), 1)) 46 | } 47 | if got, err := io.ReadAll(v.Reader()); err != nil || string(got) != s { 48 | t.Errorf("%s: Reader = %q, %v; want %q", name, got, err, s) 49 | } 50 | if got, err := io.ReadAll(io.NewSectionReader(v, 0, int64(len(s)))); err != nil || string(got) != s { 51 | t.Errorf("%s: SectionReader of ReaderAt = %q, %v; want %q", name, got, err, s) 52 | } 53 | var dest bytes.Buffer 54 | if _, err := v.WriteTo(&dest); err != nil || !bytes.Equal(dest.Bytes(), []byte(s)) { 55 | t.Errorf("%s: WriteTo = %q, %v; want %q", name, dest.Bytes(), err, s) 56 | } 57 | } 58 | } 59 | } 60 | 61 | func TestByteViewEqual(t *testing.T) { 62 | tests := []struct { 63 | a interface{} // string or []byte 64 | b interface{} // string or []byte 65 | want bool 66 | }{ 67 | {"x", "x", true}, 68 | {"x", "y", false}, 69 | {"x", "yy", false}, 70 | {[]byte("x"), []byte("x"), true}, 71 | {[]byte("x"), []byte("y"), false}, 72 | {[]byte("x"), []byte("yy"), false}, 73 | {[]byte("x"), "x", true}, 74 | {[]byte("x"), "y", false}, 75 | {[]byte("x"), "yy", false}, 76 | {"x", []byte("x"), true}, 77 | {"x", []byte("y"), false}, 78 | {"x", []byte("yy"), false}, 79 | } 80 | for i, tt := range tests { 81 | va := transport.ByteViewFrom(tt.a) 82 | if bytes, ok := tt.b.([]byte); ok { 83 | if got := va.EqualBytes(bytes); got != tt.want { 84 | t.Errorf("%d. EqualBytes = %v; want %v", i, got, tt.want) 85 | } 86 | } else { 87 | if got := va.EqualString(tt.b.(string)); got != tt.want { 88 | t.Errorf("%d. EqualString = %v; want %v", i, got, tt.want) 89 | } 90 | } 91 | if got := va.Equal(transport.ByteViewFrom(tt.b)); got != tt.want { 92 | t.Errorf("%d. Equal = %v; want %v", i, got, tt.want) 93 | } 94 | } 95 | } 96 | 97 | func TestByteViewSlice(t *testing.T) { 98 | tests := []struct { 99 | in string 100 | from int 101 | to interface{} // nil to mean the end (SliceFrom); else int 102 | want string 103 | }{ 104 | { 105 | in: "abc", 106 | from: 1, 107 | to: 2, 108 | want: "b", 109 | }, 110 | { 111 | in: "abc", 112 | from: 1, 113 | want: "bc", 114 | }, 115 | { 116 | in: "abc", 117 | to: 2, 118 | want: "ab", 119 | }, 120 | } 121 | for i, tt := range tests { 122 | for _, v := range []transport.ByteView{transport.ByteViewFrom([]byte(tt.in)), transport.ByteViewFrom(tt.in)} { 123 | name := fmt.Sprintf("test %d, view %+v", i, v) 124 | if tt.to != nil { 125 | v = v.Slice(tt.from, tt.to.(int)) 126 | } else { 127 | v = v.SliceFrom(tt.from) 128 | } 129 | if v.String() != tt.want { 130 | t.Errorf("%s: got %q; want %q", name, v.String(), tt.want) 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /internal/lru/lru.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package lru implements an LRU cache. 18 | package lru 19 | 20 | import ( 21 | "container/list" 22 | "time" 23 | ) 24 | 25 | type NowFunc func() time.Time 26 | 27 | // Cache is an LRU cache. It is not safe for concurrent access. 28 | type Cache struct { 29 | // MaxEntries is the maximum number of cache entries before 30 | // an item is evicted. Zero means no limit. 31 | MaxEntries int 32 | 33 | // OnEvicted optionally specifies a callback function to be 34 | // executed when an entry is purged from the cache. 35 | OnEvicted func(key Key, value interface{}) 36 | 37 | // Now is the Now() function the cache will use to determine 38 | // the current time which is used to calculate expired values 39 | // Defaults to time.Now() 40 | Now NowFunc 41 | 42 | ll *list.List 43 | cache map[interface{}]*list.Element 44 | } 45 | 46 | // A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators 47 | type Key interface{} 48 | 49 | type entry struct { 50 | key Key 51 | value interface{} 52 | expire time.Time 53 | } 54 | 55 | // New creates a new Cache. 56 | // If maxEntries is zero, the cache has no limit and it's assumed 57 | // that eviction is done by the caller. 58 | func New(maxEntries int) *Cache { 59 | return &Cache{ 60 | MaxEntries: maxEntries, 61 | ll: list.New(), 62 | cache: make(map[interface{}]*list.Element), 63 | Now: time.Now, 64 | } 65 | } 66 | 67 | // Add adds a value to the cache. 68 | func (c *Cache) Add(key Key, value interface{}, expire time.Time) { 69 | if c.cache == nil { 70 | c.cache = make(map[interface{}]*list.Element) 71 | c.ll = list.New() 72 | } 73 | if ee, ok := c.cache[key]; ok { 74 | eee := ee.Value.(*entry) 75 | if c.OnEvicted != nil { 76 | c.OnEvicted(key, eee.value) 77 | } 78 | c.ll.MoveToFront(ee) 79 | eee.expire = expire 80 | eee.value = value 81 | return 82 | } 83 | ele := c.ll.PushFront(&entry{key, value, expire}) 84 | c.cache[key] = ele 85 | if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { 86 | c.RemoveOldest() 87 | } 88 | } 89 | 90 | // Get looks up a key's value from the cache. 91 | func (c *Cache) Get(key Key) (value interface{}, ok bool) { 92 | if c.cache == nil { 93 | return 94 | } 95 | if ele, hit := c.cache[key]; hit { 96 | entry := ele.Value.(*entry) 97 | // If the entry has expired, remove it from the cache 98 | if !entry.expire.IsZero() && entry.expire.Before(c.Now()) { 99 | c.removeElement(ele) 100 | return nil, false 101 | } 102 | 103 | c.ll.MoveToFront(ele) 104 | return entry.value, true 105 | } 106 | return 107 | } 108 | 109 | // Remove removes the provided key from the cache. 110 | func (c *Cache) Remove(key Key) { 111 | if c.cache == nil { 112 | return 113 | } 114 | if ele, hit := c.cache[key]; hit { 115 | c.removeElement(ele) 116 | } 117 | } 118 | 119 | // RemoveOldest removes the oldest item from the cache. 120 | func (c *Cache) RemoveOldest() { 121 | if c.cache == nil { 122 | return 123 | } 124 | ele := c.ll.Back() 125 | if ele != nil { 126 | c.removeElement(ele) 127 | } 128 | } 129 | 130 | func (c *Cache) removeElement(e *list.Element) { 131 | c.ll.Remove(e) 132 | kv := e.Value.(*entry) 133 | delete(c.cache, kv.key) 134 | if c.OnEvicted != nil { 135 | c.OnEvicted(kv.key, kv.value) 136 | } 137 | } 138 | 139 | // Len returns the number of items in the cache. 140 | func (c *Cache) Len() int { 141 | if c.cache == nil { 142 | return 0 143 | } 144 | return c.ll.Len() 145 | } 146 | 147 | // Clear purges all stored items from the cache. 148 | func (c *Cache) Clear() { 149 | if c.OnEvicted != nil { 150 | for _, e := range c.cache { 151 | kv := e.Value.(*entry) 152 | c.OnEvicted(kv.key, kv.value) 153 | } 154 | } 155 | c.ll = nil 156 | c.cache = nil 157 | } 158 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright Derrick J Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package groupcache 19 | 20 | import ( 21 | "sync" 22 | "time" 23 | 24 | "github.com/groupcache/groupcache-go/v3/internal/lru" 25 | "github.com/groupcache/groupcache-go/v3/transport" 26 | ) 27 | 28 | // Cache is the interface a cache should implement 29 | type Cache interface { 30 | // Get returns a ByteView from the cache 31 | Get(string) (transport.ByteView, bool) 32 | // Add adds a ByteView to the cache 33 | Add(string, transport.ByteView) 34 | // Stats returns stats about the current cache 35 | Stats() CacheStats 36 | // Remove removes a ByteView from the cache 37 | Remove(string) 38 | // Bytes returns the total number of bytes in the cache 39 | Bytes() int64 40 | // Close closes the cache. The implementation should shut down any 41 | // background operations. 42 | Close() 43 | } 44 | 45 | // nowFunc returns the current time which is used by the LRU to 46 | // determine if the value has expired. This can be overridden by 47 | // tests to ensure items are evicted when expired. 48 | var nowFunc lru.NowFunc = time.Now 49 | 50 | // mutexCache is a wrapper around an *lru.Cache that uses a mutex for 51 | // synchronization, makes values always be ByteView, counts the size 52 | // of all keys and values and automatically evicts them to keep the 53 | // cache under the requested bytes limit. 54 | type mutexCache struct { 55 | mu sync.RWMutex 56 | lru *lru.Cache 57 | bytes int64 // total bytes of all keys and values 58 | hits, gets, evictions int64 59 | maxBytes int64 60 | } 61 | 62 | // newMutexCache creates a new cache. If maxBytes == 0 then size of the cache is unbounded. 63 | func newMutexCache(maxBytes int64) *mutexCache { 64 | return &mutexCache{ 65 | maxBytes: maxBytes, 66 | } 67 | } 68 | 69 | func (m *mutexCache) Stats() CacheStats { 70 | m.mu.RLock() 71 | defer m.mu.RUnlock() 72 | return CacheStats{ 73 | Bytes: m.bytes, 74 | Items: m.itemsLocked(), 75 | Gets: m.gets, 76 | Hits: m.hits, 77 | Evictions: m.evictions, 78 | } 79 | } 80 | 81 | func (m *mutexCache) Add(key string, value transport.ByteView) { 82 | m.mu.Lock() 83 | defer m.mu.Unlock() 84 | if m.lru == nil { 85 | m.lru = &lru.Cache{ 86 | Now: nowFunc, 87 | OnEvicted: func(key lru.Key, value interface{}) { 88 | val := value.(transport.ByteView) 89 | m.bytes -= int64(len(key.(string))) + int64(val.Len()) 90 | m.evictions++ 91 | }, 92 | } 93 | } 94 | m.lru.Add(key, value, value.Expire()) 95 | m.bytes += int64(len(key)) + int64(value.Len()) 96 | m.removeOldest() 97 | } 98 | 99 | func (m *mutexCache) Get(key string) (value transport.ByteView, ok bool) { 100 | m.mu.Lock() 101 | defer m.mu.Unlock() 102 | m.gets++ 103 | if m.lru == nil { 104 | return 105 | } 106 | vi, ok := m.lru.Get(key) 107 | if !ok { 108 | return 109 | } 110 | m.hits++ 111 | return vi.(transport.ByteView), true 112 | } 113 | 114 | func (m *mutexCache) Remove(key string) { 115 | m.mu.Lock() 116 | defer m.mu.Unlock() 117 | if m.lru == nil { 118 | return 119 | } 120 | m.lru.Remove(key) 121 | } 122 | 123 | func (m *mutexCache) Bytes() int64 { 124 | m.mu.RLock() 125 | defer m.mu.RUnlock() 126 | return m.bytes 127 | } 128 | 129 | func (m *mutexCache) Close() { 130 | // Do nothing 131 | } 132 | 133 | // removeOldest removes the oldest items in the cache until the number of bytes is 134 | // under the maxBytes limit. Must be called from a function which already maintains 135 | // a lock. 136 | func (m *mutexCache) removeOldest() { 137 | if m.maxBytes == 0 { 138 | return 139 | } 140 | for { 141 | if m.bytes <= m.maxBytes { 142 | return 143 | } 144 | if m.lru != nil { 145 | m.lru.RemoveOldest() 146 | } 147 | } 148 | } 149 | 150 | func (m *mutexCache) itemsLocked() int64 { 151 | if m.lru == nil { 152 | return 0 153 | } 154 | return int64(m.lru.Len()) 155 | } 156 | -------------------------------------------------------------------------------- /internal/lru/lru_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lru 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | type simpleStruct struct { 26 | int 27 | string 28 | } 29 | 30 | type complexStruct struct { 31 | int 32 | simpleStruct 33 | } 34 | 35 | var getTests = []struct { 36 | name string 37 | keyToAdd interface{} 38 | keyToGet interface{} 39 | expectedOk bool 40 | }{ 41 | {"string_hit", "myKey", "myKey", true}, 42 | {"string_miss", "myKey", "nonsense", false}, 43 | {"simple_struct_hit", simpleStruct{1, "two"}, simpleStruct{1, "two"}, true}, 44 | {"simple_struct_miss", simpleStruct{1, "two"}, simpleStruct{0, "noway"}, false}, 45 | {"complex_struct_hit", complexStruct{1, simpleStruct{2, "three"}}, 46 | complexStruct{1, simpleStruct{2, "three"}}, true}, 47 | } 48 | 49 | func TestAdd_evictsOldAndReplaces(t *testing.T) { 50 | var evictedKey Key 51 | var evictedValue interface{} 52 | lru := New(0) 53 | lru.OnEvicted = func(key Key, value interface{}) { 54 | evictedKey = key 55 | evictedValue = value 56 | } 57 | lru.Add("myKey", 1234, time.Time{}) 58 | lru.Add("myKey", 1235, time.Time{}) 59 | 60 | newVal, ok := lru.Get("myKey") 61 | if !ok { 62 | t.Fatalf("%s: cache hit = %v; want %v", t.Name(), ok, !ok) 63 | } 64 | if newVal != 1235 { 65 | t.Fatalf("%s: cache hit = %v; want %v", t.Name(), newVal, 1235) 66 | } 67 | if evictedKey != "myKey" { 68 | t.Fatalf("%s: evictedKey = %v; want %v", t.Name(), evictedKey, "myKey") 69 | } 70 | if evictedValue != 1234 { 71 | t.Fatalf("%s: evictedValue = %v; want %v", t.Name(), evictedValue, 1234) 72 | } 73 | } 74 | 75 | func TestGet(t *testing.T) { 76 | for _, tt := range getTests { 77 | lru := New(0) 78 | lru.Add(tt.keyToAdd, 1234, time.Time{}) 79 | val, ok := lru.Get(tt.keyToGet) 80 | if ok != tt.expectedOk { 81 | t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok) 82 | } else if ok && val != 1234 { 83 | t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val) 84 | } 85 | } 86 | } 87 | 88 | func TestRemove(t *testing.T) { 89 | lru := New(0) 90 | lru.Add("myKey", 1234, time.Time{}) 91 | if val, ok := lru.Get("myKey"); !ok { 92 | t.Fatal("TestRemove returned no match") 93 | } else if val != 1234 { 94 | t.Fatalf("TestRemove failed. Expected %d, got %v", 1234, val) 95 | } 96 | 97 | lru.Remove("myKey") 98 | if _, ok := lru.Get("myKey"); ok { 99 | t.Fatal("TestRemove returned a removed entry") 100 | } 101 | } 102 | 103 | func TestEvict(t *testing.T) { 104 | evictedKeys := make([]Key, 0) 105 | onEvictedFun := func(key Key, value interface{}) { 106 | evictedKeys = append(evictedKeys, key) 107 | } 108 | 109 | lru := New(20) 110 | lru.OnEvicted = onEvictedFun 111 | for i := 0; i < 22; i++ { 112 | lru.Add(fmt.Sprintf("myKey%d", i), 1234, time.Time{}) 113 | } 114 | 115 | if len(evictedKeys) != 2 { 116 | t.Fatalf("got %d evicted keys; want 2", len(evictedKeys)) 117 | } 118 | if evictedKeys[0] != Key("myKey0") { 119 | t.Fatalf("got %v in first evicted key; want %s", evictedKeys[0], "myKey0") 120 | } 121 | if evictedKeys[1] != Key("myKey1") { 122 | t.Fatalf("got %v in second evicted key; want %s", evictedKeys[1], "myKey1") 123 | } 124 | } 125 | 126 | func TestExpire(t *testing.T) { 127 | var tests = []struct { 128 | name string 129 | key interface{} 130 | expectedOk bool 131 | expire time.Duration 132 | wait time.Duration 133 | }{ 134 | {"not-expired", "myKey", true, time.Second * 1, time.Duration(0)}, 135 | {"expired", "expiredKey", false, time.Millisecond * 100, time.Millisecond * 150}, 136 | } 137 | 138 | for _, tt := range tests { 139 | lru := New(0) 140 | lru.Add(tt.key, 1234, time.Now().Add(tt.expire)) 141 | time.Sleep(tt.wait) 142 | val, ok := lru.Get(tt.key) 143 | if ok != tt.expectedOk { 144 | t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok) 145 | } else if ok && val != 1234 { 146 | t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /transport/peer/picker.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. 3 | Copyright 2024 Derrick J Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package peer 19 | 20 | import ( 21 | "crypto/md5" 22 | "fmt" 23 | "sort" 24 | "strconv" 25 | 26 | "github.com/segmentio/fasthash/fnv1" 27 | ) 28 | 29 | // DefaultReplicas is set at 50, which is enough to create a reasonably even key distribution. 30 | // As the size of your cluster increases, you may want to consider increasing the number 31 | // of replicas if you are having un-even distribution issues. 32 | const DefaultReplicas = 50 33 | 34 | // Info represents information about a peer. This struct is intended to be used by peer discovery mechanisms 35 | // when calling `Instance.SetPeers()` 36 | type Info struct { 37 | Address string 38 | IsSelf bool 39 | } 40 | 41 | // HashFn represents a function that takes a byte slice as input and returns a uint64 hash value. 42 | type HashFn func(data []byte) uint64 43 | 44 | // Options represents the settings for the peer picker 45 | type Options struct { 46 | // - HashFn: a function type that takes a byte slice and returns a uint64 47 | HashFn HashFn 48 | // - Replicas: an integer representing the number of replicas 49 | Replicas int 50 | } 51 | 52 | // Picker represents a data structure for picking a peer based on a given key. 53 | // It uses consistent hashing to distribute keys among the available peers. 54 | type Picker struct { 55 | opts Options 56 | // keys is a stored list of all keys in the ring 57 | keys []int 58 | // hashMap contains the key ring 59 | hashMap map[int]Client 60 | // clientMap contains a list of added clients by key 61 | clientMap map[string]Client 62 | } 63 | 64 | // NewPicker creates a new Picker instance with the given options. 65 | // It initializes the replicas, hash function, and hash map. 66 | // If the hash function is not provided in the options, it defaults to fnv1.HashBytes64. 67 | // Returns the newly created Picker instance. 68 | // 69 | // Example usage: 70 | // 71 | // picker := NewPicker(Options{ 72 | // HashFn: hashFunction, 73 | // Replicas: 100, 74 | // }) 75 | // ... 76 | func NewPicker(opts Options) *Picker { 77 | m := &Picker{ 78 | opts: opts, 79 | hashMap: make(map[int]Client), 80 | clientMap: make(map[string]Client), 81 | } 82 | if m.opts.HashFn == nil { 83 | m.opts.HashFn = fnv1.HashBytes64 84 | } 85 | if m.opts.Replicas == 0 { 86 | m.opts.Replicas = DefaultReplicas 87 | } 88 | return m 89 | } 90 | 91 | // PickPeer picks the appropriate peer for the given key. Returns true if the chosen client is to a remote peer. 92 | func (p *Picker) PickPeer(key string) (client Client, isRemote bool) { 93 | if p.IsEmpty() { 94 | return nil, false 95 | } 96 | 97 | c := p.Get(key) 98 | return c, !c.PeerInfo().IsSelf 99 | } 100 | 101 | // GetAll returns all clients added to the Picker 102 | func (p *Picker) GetAll() []Client { 103 | var results []Client 104 | for _, v := range p.clientMap { 105 | results = append(results, v) 106 | } 107 | return results 108 | } 109 | 110 | // IsEmpty returns true if there are no items available. 111 | func (p *Picker) IsEmpty() bool { 112 | return len(p.keys) == 0 113 | } 114 | 115 | // Add a client to the peer picker 116 | func (p *Picker) Add(client Client) { 117 | p.clientMap[client.HashKey()] = client 118 | for i := 0; i < p.opts.Replicas; i++ { 119 | hash := int(p.opts.HashFn([]byte(fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(i)+client.HashKey())))))) 120 | p.keys = append(p.keys, hash) 121 | p.hashMap[hash] = client 122 | } 123 | sort.Ints(p.keys) 124 | } 125 | 126 | // Get the closest client in the hash to the provided key. 127 | func (p *Picker) Get(key string) Client { 128 | if p.IsEmpty() { 129 | return nil 130 | } 131 | 132 | hash := int(p.opts.HashFn([]byte(key))) 133 | 134 | // Binary search for appropriate replica. 135 | idx := sort.Search(len(p.keys), func(i int) bool { return p.keys[i] >= hash }) 136 | 137 | // Means we have cycled back to the first replica. 138 | if idx == len(p.keys) { 139 | idx = 0 140 | } 141 | 142 | return p.hashMap[p.keys[idx]] 143 | } 144 | -------------------------------------------------------------------------------- /cluster/cluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package cluster contains convince functions which make managing the creation of multiple groupcache instances 19 | simple. To start a local cluster of groupcache daemons suitable for testing you can call cluster.Start() 20 | or cluster.StartWith(). See cluster_test.go for more examples. 21 | 22 | err := cluster.Start(context.Background(), 2, groupcache.Options{}) 23 | require.NoError(t, err) 24 | 25 | assert.Equal(t, 2, len(cluster.ListPeers())) 26 | assert.Equal(t, 2, len(cluster.ListDaemons())) 27 | err = cluster.Shutdown(context.Background()) 28 | require.NoError(t, err) 29 | */ 30 | package cluster 31 | 32 | import ( 33 | "context" 34 | "errors" 35 | "fmt" 36 | 37 | "github.com/groupcache/groupcache-go/v3" 38 | "github.com/groupcache/groupcache-go/v3/transport" 39 | "github.com/groupcache/groupcache-go/v3/transport/peer" 40 | ) 41 | 42 | var _daemons []*groupcache.Daemon 43 | var _peers []peer.Info 44 | 45 | // ListPeers returns a list of all peers in the cluster 46 | func ListPeers() []peer.Info { 47 | return _peers 48 | } 49 | 50 | // ListDaemons returns a list of all daemons in the cluster 51 | func ListDaemons() []*groupcache.Daemon { 52 | return _daemons 53 | } 54 | 55 | // DaemonAt returns a specific daemon 56 | func DaemonAt(idx int) *groupcache.Daemon { 57 | return _daemons[idx] 58 | } 59 | 60 | // PeerAt returns a specific peer 61 | func PeerAt(idx int) peer.Info { 62 | return _peers[idx] 63 | } 64 | 65 | // FindOwningDaemon finds the daemon which owns the key provided 66 | func FindOwningDaemon(key string) *groupcache.Daemon { 67 | if len(_daemons) == 0 { 68 | panic("'_daemon' is empty; start a cluster with Start() or StartWith()") 69 | } 70 | 71 | c, isRemote := _daemons[0].GetInstance().PickPeer(key) 72 | if !isRemote { 73 | return _daemons[0] 74 | } 75 | 76 | for i, d := range _daemons { 77 | if d.ListenAddress() == c.PeerInfo().Address { 78 | return _daemons[i] 79 | } 80 | } 81 | panic(fmt.Sprintf("failed to find daemon which owns '%s'", key)) 82 | } 83 | 84 | // Start a local cluster 85 | func Start(ctx context.Context, numInstances int, opts groupcache.Options) error { 86 | var peers []peer.Info 87 | port := 1111 88 | for i := 0; i < numInstances; i++ { 89 | peers = append(peers, peer.Info{ 90 | Address: fmt.Sprintf("localhost:%d", port), 91 | }) 92 | port += 1 93 | } 94 | return StartWith(ctx, peers, opts) 95 | } 96 | 97 | // StartWith a local cluster with specific addresses 98 | func StartWith(ctx context.Context, peers []peer.Info, opts groupcache.Options) error { 99 | if len(_daemons) != 0 || len(_peers) != 0 { 100 | return errors.New("StartWith: cluster already running; shutdown the previous cluster") 101 | } 102 | 103 | var parent transport.Transport 104 | if opts.Transport == nil { 105 | parent = transport.NewHttpTransport(transport.HttpTransportOptions{}) 106 | } else { 107 | parent = opts.Transport 108 | } 109 | 110 | for _, p := range peers { 111 | d, err := groupcache.ListenAndServe(ctx, p.Address, opts) 112 | if err != nil { 113 | return fmt.Errorf("StartWith: while starting daemon for '%s': %w", p.Address, err) 114 | } 115 | 116 | // Create a new instance of the parent transport 117 | opts.Transport = parent.New() 118 | 119 | // Add the peers and daemons to the package level variables 120 | _daemons = append(_daemons, d) 121 | _peers = append(_peers, peer.Info{ 122 | Address: d.ListenAddress(), 123 | IsSelf: p.IsSelf, 124 | }) 125 | } 126 | 127 | // Tell each daemon about the other peers 128 | for _, d := range _daemons { 129 | if err := d.SetPeers(ctx, _peers); err != nil { 130 | return fmt.Errorf("StartWith: during SetPeers(): %w", err) 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | // Restart the cluster 137 | func Restart(ctx context.Context) error { 138 | for i := 0; i < len(_daemons); i++ { 139 | if err := _daemons[i].Shutdown(ctx); err != nil { 140 | return err 141 | } 142 | if err := _daemons[i].Start(ctx); err != nil { 143 | return err 144 | } 145 | _ = _daemons[i].GetInstance().SetPeers(ctx, _peers) 146 | } 147 | return nil 148 | } 149 | 150 | // Shutdown all daemons in the cluster 151 | func Shutdown(ctx context.Context) error { 152 | for _, d := range _daemons { 153 | if err := d.Shutdown(ctx); err != nil { 154 | return err 155 | } 156 | } 157 | _peers = nil 158 | _daemons = nil 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /transport/byteview.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transport 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "io" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | // A ByteView holds an immutable view of bytes. 28 | // Internally it wraps either a []byte or a string, 29 | // but that detail is invisible to callers. 30 | // 31 | // A ByteView is meant to be used as a value type, not 32 | // a pointer (like a time.Time). 33 | type ByteView struct { 34 | // If b is non-nil, b is used, else s is used. 35 | b []byte 36 | s string 37 | e time.Time 38 | } 39 | 40 | func ByteViewFrom(x any) ByteView { 41 | if b, ok := x.([]byte); ok { 42 | return ByteView{b: b} 43 | } 44 | return ByteView{s: x.(string)} 45 | } 46 | 47 | func ByteViewWithExpire(b []byte, expire time.Time) ByteView { 48 | return ByteView{ 49 | e: expire, 50 | b: b, 51 | } 52 | } 53 | 54 | // Expire returns the expiration time associated with this view 55 | func (v ByteView) Expire() time.Time { 56 | return v.e 57 | } 58 | 59 | // Len returns the view's length. 60 | func (v ByteView) Len() int { 61 | if v.b != nil { 62 | return len(v.b) 63 | } 64 | return len(v.s) 65 | } 66 | 67 | // ByteSlice returns a copy of the data as a byte slice. 68 | func (v ByteView) ByteSlice() []byte { 69 | if v.b != nil { 70 | return cloneBytes(v.b) 71 | } 72 | return []byte(v.s) 73 | } 74 | 75 | // String returns the data as a string, making a copy if necessary. 76 | func (v ByteView) String() string { 77 | if v.b != nil { 78 | return string(v.b) 79 | } 80 | return v.s 81 | } 82 | 83 | // At returns the byte at index i. 84 | func (v ByteView) At(i int) byte { 85 | if v.b != nil { 86 | return v.b[i] 87 | } 88 | return v.s[i] 89 | } 90 | 91 | // Slice slices the view between the provided from and to indices. 92 | func (v ByteView) Slice(from, to int) ByteView { 93 | if v.b != nil { 94 | return ByteView{b: v.b[from:to]} 95 | } 96 | return ByteView{s: v.s[from:to]} 97 | } 98 | 99 | // SliceFrom slices the view from the provided index until the end. 100 | func (v ByteView) SliceFrom(from int) ByteView { 101 | if v.b != nil { 102 | return ByteView{b: v.b[from:]} 103 | } 104 | return ByteView{s: v.s[from:]} 105 | } 106 | 107 | // Copy copies b into dest and returns the number of bytes copied. 108 | func (v ByteView) Copy(dest []byte) int { 109 | if v.b != nil { 110 | return copy(dest, v.b) 111 | } 112 | return copy(dest, v.s) 113 | } 114 | 115 | // Equal returns whether the bytes in b are the same as the bytes in 116 | // b2. 117 | func (v ByteView) Equal(b2 ByteView) bool { 118 | if b2.b == nil { 119 | return v.EqualString(b2.s) 120 | } 121 | return v.EqualBytes(b2.b) 122 | } 123 | 124 | // EqualString returns whether the bytes in b are the same as the bytes 125 | // in s. 126 | func (v ByteView) EqualString(s string) bool { 127 | if v.b == nil { 128 | return v.s == s 129 | } 130 | l := v.Len() 131 | if len(s) != l { 132 | return false 133 | } 134 | for i, bi := range v.b { 135 | if bi != s[i] { 136 | return false 137 | } 138 | } 139 | return true 140 | } 141 | 142 | // EqualBytes returns whether the bytes in b are the same as the bytes 143 | // in b2. 144 | func (v ByteView) EqualBytes(b2 []byte) bool { 145 | if v.b != nil { 146 | return bytes.Equal(v.b, b2) 147 | } 148 | l := v.Len() 149 | if len(b2) != l { 150 | return false 151 | } 152 | for i, bi := range b2 { 153 | if bi != v.s[i] { 154 | return false 155 | } 156 | } 157 | return true 158 | } 159 | 160 | // Reader returns an io.ReadSeeker for the bytes in v. 161 | func (v ByteView) Reader() io.ReadSeeker { 162 | if v.b != nil { 163 | return bytes.NewReader(v.b) 164 | } 165 | return strings.NewReader(v.s) 166 | } 167 | 168 | // ReadAt implements io.ReaderAt on the bytes in v. 169 | func (v ByteView) ReadAt(p []byte, off int64) (n int, err error) { 170 | if off < 0 { 171 | return 0, errors.New("view: invalid offset") 172 | } 173 | if off >= int64(v.Len()) { 174 | return 0, io.EOF 175 | } 176 | n = v.SliceFrom(int(off)).Copy(p) 177 | if n < len(p) { 178 | err = io.EOF 179 | } 180 | return 181 | } 182 | 183 | // WriteTo implements io.WriterTo on the bytes in v. 184 | func (v ByteView) WriteTo(w io.Writer) (n int64, err error) { 185 | var m int 186 | if v.b != nil { 187 | m, err = w.Write(v.b) 188 | } else { 189 | m, err = io.WriteString(w, v.s) 190 | } 191 | if err == nil && m < v.Len() { 192 | err = io.ErrShortWrite 193 | } 194 | n = int64(m) 195 | return 196 | } 197 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= 4 | github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 5 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 6 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 7 | github.com/gammazero/deque v1.2.0 h1:scEFO8Uidhw6KDU5qg1HA5fYwM0+us2qdeJqm43bitU= 8 | github.com/gammazero/deque v1.2.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg= 9 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 10 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 11 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 12 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 13 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 14 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 15 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/kapetan-io/tackle v0.13.0 h1:kcQTbgZN+4T89ktqlpW2TBATjiBmfjIyuZUukvRrYZU= 19 | github.com/kapetan-io/tackle v0.13.0/go.mod h1:5ZGq3U/Qgpq0ccxyx2+Zovg2ceM9yl6DOVL2R90of4g= 20 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 21 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 22 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 23 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 24 | github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc= 25 | github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 29 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 30 | github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= 31 | github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= 32 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 33 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 34 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 35 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 36 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= 37 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= 38 | go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 39 | go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 40 | go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 41 | go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 42 | go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 43 | go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 44 | go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 45 | go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 46 | go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 47 | go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 48 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 49 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 50 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 51 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 52 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 53 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 54 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 55 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 58 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 59 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 60 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | -------------------------------------------------------------------------------- /transport/mock_transport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transport 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "slices" 23 | "strings" 24 | 25 | "github.com/groupcache/groupcache-go/v3/transport/pb" 26 | "github.com/groupcache/groupcache-go/v3/transport/peer" 27 | ) 28 | 29 | // MockTransport is intended to be used as a singleton. Pass a new instance of this singleton into groupcache.New() 30 | // by calling MockTransport.New(). You can then inspect the parent MockTransport for call statistics of the children 31 | // in tests. This Transport is NOT THREAD SAFE 32 | // Example usage: 33 | // 34 | // t := NewMockTransport() 35 | // i := groupcache.New(groupcache.Options{Transport: t.New()}) 36 | type MockTransport struct { 37 | instances map[string]GroupCacheInstance 38 | transports map[string]*MockTransport 39 | calls map[string]*peerStats 40 | register GroupCacheInstance 41 | parent *MockTransport 42 | address string 43 | } 44 | 45 | func NewMockTransport() *MockTransport { 46 | m := &MockTransport{ 47 | instances: make(map[string]GroupCacheInstance), 48 | transports: make(map[string]*MockTransport), 49 | calls: make(map[string]*peerStats), 50 | } 51 | // We do this to avoid accidental nil deref errors if MockTransport.New() is never called. 52 | m.parent = m 53 | return m 54 | } 55 | 56 | func (t *MockTransport) Register(i GroupCacheInstance) { 57 | t.register = i 58 | } 59 | func (t *MockTransport) New() Transport { 60 | m := NewMockTransport() 61 | // Register us as a parent of the new transport 62 | m.parent = t 63 | return m 64 | } 65 | 66 | func (t *MockTransport) ListenAddress() string { 67 | return t.address 68 | } 69 | 70 | func (t *MockTransport) ListenAndServe(_ context.Context, address string) error { 71 | t.parent.instances[address] = t.register 72 | t.parent.transports[address] = t 73 | t.address = address 74 | return nil 75 | } 76 | 77 | func (t *MockTransport) Shutdown(_ context.Context) error { 78 | delete(t.parent.instances, t.address) 79 | delete(t.parent.transports, t.address) 80 | return nil 81 | } 82 | 83 | func (t *MockTransport) Reset() { 84 | t.calls = make(map[string]*peerStats) 85 | } 86 | 87 | func (t *MockTransport) Report(method string) string { 88 | stats, ok := t.calls[method] 89 | if !ok { 90 | return "" 91 | } 92 | return stats.Report() 93 | } 94 | 95 | func (t *MockTransport) NewClient(ctx context.Context, peer peer.Info) (peer.Client, error) { 96 | return &MockClient{ 97 | peer: peer, 98 | transport: t, 99 | }, nil 100 | } 101 | 102 | type MockClient struct { 103 | transport *MockTransport 104 | peer peer.Info 105 | } 106 | 107 | func (c *MockClient) addCall(method string, count int) { 108 | m, ok := c.transport.parent.calls[method] 109 | if !ok { 110 | c.transport.parent.calls[method] = &peerStats{ 111 | stats: make(map[string]int), 112 | } 113 | m = c.transport.parent.calls[method] 114 | } 115 | m.Add(c.peer.Address, count) 116 | } 117 | 118 | func (c *MockClient) Get(ctx context.Context, in *pb.GetRequest, out *pb.GetResponse) error { 119 | g, ok := c.transport.parent.instances[c.peer.Address] 120 | if !ok { 121 | return fmt.Errorf("dial tcp %s connect: connection refused'", c.peer.Address) 122 | } 123 | 124 | c.addCall("Get", 1) 125 | 126 | var b []byte 127 | value := AllocatingByteSliceSink(&b) 128 | if err := g.GetGroup(in.GetGroup()).Get(ctx, in.GetKey(), value); err != nil { 129 | return err 130 | } 131 | out.Value = b 132 | return nil 133 | } 134 | 135 | func (c *MockClient) Remove(ctx context.Context, in *pb.GetRequest) error { 136 | c.addCall("Remove", 1) 137 | // TODO: Implement when needed 138 | return nil 139 | } 140 | 141 | func (c *MockClient) Set(ctx context.Context, in *pb.SetRequest) error { 142 | c.addCall("Set", 1) 143 | // TODO: Implement when needed 144 | return nil 145 | } 146 | 147 | func (c *MockClient) PeerInfo() peer.Info { 148 | c.addCall("PeerInfo", 1) 149 | return c.peer 150 | } 151 | 152 | func (c *MockClient) HashKey() string { 153 | c.addCall("HashKey", 1) 154 | return c.peer.Address 155 | } 156 | 157 | type peerStats struct { 158 | stats map[string]int 159 | } 160 | 161 | // Add adds a count to the peerStats Map 162 | func (s *peerStats) Add(key string, count int) { 163 | s.stats[key] += count 164 | } 165 | 166 | // Report returns a string representation of the stats in the format : 167 | // Example: "peer1:50 peer2:48 peer3:45" 168 | func (s *peerStats) Report() string { 169 | var b strings.Builder 170 | 171 | var sorted []string 172 | for k := range s.stats { 173 | sorted = append(sorted, k) 174 | } 175 | 176 | // Map keys have no guaranteed order, so we sort the keys here. 177 | slices.Sort(sorted) 178 | for i := 0; i < len(sorted); i++ { 179 | b.WriteString(fmt.Sprintf("%s = %d ", sorted[i], s.stats[sorted[i]])) 180 | } 181 | return strings.TrimSpace(b.String()) 182 | } 183 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package groupcache_test 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "log" 23 | "log/slog" 24 | "net/http" 25 | "time" 26 | 27 | "github.com/segmentio/fasthash/fnv1" 28 | 29 | "github.com/groupcache/groupcache-go/v3" 30 | "github.com/groupcache/groupcache-go/v3/transport" 31 | "github.com/groupcache/groupcache-go/v3/transport/peer" 32 | ) 33 | 34 | // ExampleNew demonstrates starting a groupcache http instance with its own 35 | // listener. 36 | // nolint 37 | func ExampleNew() { 38 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 39 | 40 | // Starts an instance of groupcache with the provided transport 41 | d, err := groupcache.ListenAndServe(ctx, "192.168.1.1:8080", groupcache.Options{ 42 | // If transport is nil, defaults to HttpTransport 43 | Transport: nil, 44 | // The following are all optional 45 | HashFn: fnv1.HashBytes64, 46 | Logger: slog.Default(), 47 | Replicas: 50, 48 | }) 49 | cancel() 50 | if err != nil { 51 | log.Fatal("while starting server on 192.168.1.1:8080") 52 | } 53 | 54 | // Create a new group cache with a max cache size of 3MB 55 | group, err := d.NewGroup("users", 3000000, groupcache.GetterFunc( 56 | func(ctx context.Context, id string, dest transport.Sink) error { 57 | // Set the user in the groupcache to expire after 5 minutes 58 | if err := dest.SetString("hello", time.Now().Add(time.Minute*5)); err != nil { 59 | return err 60 | } 61 | return nil 62 | }, 63 | )) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | ctx, cancel = context.WithTimeout(context.Background(), time.Second) 69 | defer cancel() 70 | 71 | var value string 72 | if err := group.Get(ctx, "12345", transport.StringSink(&value)); err != nil { 73 | log.Fatal(err) 74 | } 75 | fmt.Printf("Value: %s\n", value) 76 | 77 | // Remove the key from the groupcache 78 | if err := group.Remove(ctx, "12345"); err != nil { 79 | fmt.Printf("Remove Err: %s\n", err) 80 | log.Fatal(err) 81 | } 82 | 83 | // Shutdown the daemon 84 | _ = d.Shutdown(context.Background()) 85 | } 86 | 87 | // ExampleNewHttpTransport demonstrates how to use groupcache in a service that 88 | // is already listening for HTTP requests. 89 | // nolint 90 | func ExampleNewHttpTransport() { 91 | mux := http.NewServeMux() 92 | 93 | // Add endpoints specific to our application 94 | mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) { 95 | fmt.Fprintf(w, "Hello, this is a non groupcache handler") 96 | }) 97 | 98 | // Explicitly instantiate and use the HTTP transport 99 | t := transport.NewHttpTransport( 100 | transport.HttpTransportOptions{ 101 | // BasePath specifies the HTTP path that will serve groupcache requests. 102 | // If blank, it defaults to "/_groupcache/". 103 | BasePath: "/_groupcache/", 104 | // Context optionally specifies a context for the server to use when it 105 | // receives a request. 106 | Context: nil, 107 | // Client optionally provide a custom http client with TLS config 108 | Client: nil, 109 | // Scheme is is either `http` or `https` defaults to `http` 110 | Scheme: "", 111 | }, 112 | ) 113 | 114 | // Create a new groupcache instance 115 | instance := groupcache.New(groupcache.Options{ 116 | HashFn: fnv1.HashBytes64, 117 | Logger: slog.Default(), 118 | Transport: t, 119 | Replicas: 50, 120 | }) 121 | 122 | // You can set the peers manually 123 | err := instance.SetPeers(context.Background(), []peer.Info{ 124 | { 125 | Address: "192.168.1.1:8080", 126 | IsSelf: true, 127 | }, 128 | { 129 | Address: "192.168.1.1:8081", 130 | IsSelf: false, 131 | }, 132 | { 133 | Address: "192.168.1.1:8082", 134 | IsSelf: false, 135 | }, 136 | }) 137 | if err != nil { 138 | log.Fatal(err) 139 | } 140 | // OR you can register a peer discovery mechanism 141 | //d := discovery.NewK8s(discovery.K8sConfig{ 142 | // OnUpdate: instance.SetPeers, 143 | //}) 144 | //defer d.Shutdown(context.Background()) 145 | 146 | // Add the groupcache handler 147 | mux.Handle("/_groupcache/", t) 148 | 149 | server := http.Server{ 150 | Addr: "192.168.1.1:8080", 151 | Handler: mux, 152 | } 153 | 154 | // Start a HTTP server to listen for peer requests from the groupcache 155 | go func() { 156 | log.Printf("Serving....\n") 157 | if err := server.ListenAndServe(); err != nil { 158 | log.Fatal(err) 159 | } 160 | }() 161 | defer func() { _ = server.Shutdown(context.Background()) }() 162 | 163 | // Update the static peer config while groupcache is running 164 | err = instance.SetPeers(context.Background(), []peer.Info{ 165 | { 166 | Address: "192.168.1.1:8080", 167 | IsSelf: true, 168 | }, 169 | { 170 | Address: "192.168.1.1:8081", 171 | IsSelf: false, 172 | }, 173 | }) 174 | if err != nil { 175 | log.Fatal(err) 176 | } 177 | 178 | // Create a new group cache with a max cache size of 3MB 179 | group, err := instance.NewGroup("users", 3000000, groupcache.GetterFunc( 180 | func(ctx context.Context, id string, dest transport.Sink) error { 181 | // Set the user in the groupcache to expire after 5 minutes 182 | if err := dest.SetString("hello", time.Now().Add(time.Minute*5)); err != nil { 183 | return err 184 | } 185 | return nil 186 | }, 187 | )) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | 192 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 193 | defer cancel() 194 | 195 | var value string 196 | if err := group.Get(ctx, "12345", transport.StringSink(&value)); err != nil { 197 | log.Fatal(err) 198 | } 199 | fmt.Printf("Value: %s\n", value) 200 | 201 | // Remove the key from the groupcache 202 | if err := group.Remove(ctx, "12345"); err != nil { 203 | fmt.Printf("Remove Err: %s\n", err) 204 | log.Fatal(err) 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /transport/peer/picker_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. 3 | Copyright 2024 Derrick J. Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package peer_test 19 | 20 | import ( 21 | "fmt" 22 | "github.com/groupcache/groupcache-go/v3/transport/peer" 23 | "github.com/segmentio/fasthash/fnv1" 24 | "github.com/stretchr/testify/assert" 25 | "math/rand" 26 | "net" 27 | "slices" 28 | "testing" 29 | ) 30 | 31 | func TestHashing(t *testing.T) { 32 | picker := peer.NewPicker(peer.Options{Replicas: 512}) 33 | 34 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: "6"}}) 35 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: "4"}}) 36 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: "2"}}) 37 | 38 | testCases := map[string]string{ 39 | "12,000": "4", 40 | "11": "6", 41 | "500,000": "4", 42 | "1,000,000": "2", 43 | } 44 | 45 | for k, v := range testCases { 46 | if got := picker.Get(k); got.HashKey() != v { 47 | t.Errorf("Asking for %s, should have yielded %s; got %s instead", k, v, got) 48 | } 49 | } 50 | 51 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: "8"}}) 52 | 53 | testCases["11"] = "8" 54 | testCases["1,000,000"] = "8" 55 | 56 | for k, v := range testCases { 57 | if got := picker.Get(k); got.HashKey() != v { 58 | t.Errorf("Asking for %s, should have yielded %s; got %s instead", k, v, got) 59 | } 60 | } 61 | } 62 | 63 | func TestConsistency(t *testing.T) { 64 | picker1 := peer.NewPicker(peer.Options{Replicas: 1}) 65 | picker2 := peer.NewPicker(peer.Options{Replicas: 1}) 66 | 67 | picker1.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bill"}}) 68 | picker1.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bob"}}) 69 | picker1.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bonny"}}) 70 | 71 | picker2.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bob"}}) 72 | picker2.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bonny"}}) 73 | picker2.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bill"}}) 74 | 75 | if picker1.Get("Ben").HashKey() != picker2.Get("Ben").HashKey() { 76 | t.Errorf("Fetching 'Ben' from both hashes should be the same") 77 | } 78 | 79 | picker2.Add(&peer.NoOpClient{Info: peer.Info{Address: "Becky"}}) 80 | picker2.Add(&peer.NoOpClient{Info: peer.Info{Address: "Ben"}}) 81 | picker2.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bobby"}}) 82 | 83 | picker1.Add(&peer.NoOpClient{Info: peer.Info{Address: "Becky"}}) 84 | picker1.Add(&peer.NoOpClient{Info: peer.Info{Address: "Ben"}}) 85 | picker1.Add(&peer.NoOpClient{Info: peer.Info{Address: "Bobby"}}) 86 | 87 | if picker1.Get("Ben").HashKey() != picker2.Get("Ben").HashKey() || 88 | picker1.Get("Bob").HashKey() != picker2.Get("Bob").HashKey() || 89 | picker1.Get("Bonny").HashKey() != picker2.Get("Bonny").HashKey() { 90 | t.Errorf("Direct matches should always return the same entry") 91 | } 92 | } 93 | 94 | func TestDistribution(t *testing.T) { 95 | hosts := []string{"a.svc.local", "b.svc.local", "c.svc.local"} 96 | const cases = 10000 97 | 98 | strings := make([]string, cases) 99 | 100 | for i := 0; i < cases; i++ { 101 | r := rand.Int31() 102 | ip := net.IPv4(192, byte(r>>16), byte(r>>8), byte(r)) 103 | strings[i] = ip.String() 104 | } 105 | 106 | hashFuncs := map[string]peer.HashFn{ 107 | "fasthash/fnv1": fnv1.HashBytes64, 108 | } 109 | 110 | for name, hashFunc := range hashFuncs { 111 | t.Run(name, func(t *testing.T) { 112 | picker := peer.NewPicker(peer.Options{Replicas: 512, HashFn: hashFunc}) 113 | hostMap := map[string]int{} 114 | 115 | for _, host := range hosts { 116 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: host}}) 117 | hostMap[host] = 0 118 | } 119 | 120 | for i := range strings { 121 | host := picker.Get(strings[i]).HashKey() 122 | hostMap[host]++ 123 | } 124 | 125 | for host, a := range hostMap { 126 | t.Logf("host: %s, percent: %f", host, float64(a)/cases) 127 | } 128 | }) 129 | } 130 | } 131 | 132 | func TestPickPeer(t *testing.T) { 133 | picker := peer.NewPicker(peer.Options{Replicas: 512}) 134 | 135 | for _, info := range []peer.Info{ 136 | { 137 | Address: "a.svc.local", 138 | IsSelf: true, 139 | }, { 140 | Address: "b.svc.local", 141 | IsSelf: false, 142 | }, { 143 | Address: "c.svc.local", 144 | IsSelf: false, 145 | }} { 146 | picker.Add(&peer.NoOpClient{Info: info}) 147 | } 148 | 149 | p, isRemote := picker.PickPeer("Bob") 150 | assert.Equal(t, "a.svc.local", p.PeerInfo().Address) 151 | assert.True(t, p.PeerInfo().IsSelf) 152 | assert.False(t, isRemote) 153 | 154 | p, isRemote = picker.PickPeer("Johnny") 155 | assert.Equal(t, "b.svc.local", p.PeerInfo().Address) 156 | assert.False(t, p.PeerInfo().IsSelf) 157 | assert.True(t, isRemote) 158 | 159 | p, isRemote = picker.PickPeer("Rick") 160 | assert.Equal(t, "c.svc.local", p.PeerInfo().Address) 161 | assert.False(t, p.PeerInfo().IsSelf) 162 | assert.True(t, isRemote) 163 | } 164 | 165 | func TestGetAll(t *testing.T) { 166 | picker := peer.NewPicker(peer.Options{Replicas: 512}) 167 | 168 | for _, info := range []peer.Info{ 169 | { 170 | Address: "a.svc.local", 171 | IsSelf: true, 172 | }, { 173 | Address: "b.svc.local", 174 | IsSelf: false, 175 | }, { 176 | Address: "c.svc.local", 177 | IsSelf: false, 178 | }} { 179 | picker.Add(&peer.NoOpClient{Info: info}) 180 | } 181 | 182 | all := picker.GetAll() 183 | assert.Len(t, all, 3) 184 | assert.True(t, slices.ContainsFunc(all, func(c peer.Client) bool { return c.PeerInfo().Address == "a.svc.local" })) 185 | assert.True(t, slices.ContainsFunc(all, func(c peer.Client) bool { return c.PeerInfo().Address == "b.svc.local" })) 186 | assert.True(t, slices.ContainsFunc(all, func(c peer.Client) bool { return c.PeerInfo().Address == "c.svc.local" })) 187 | } 188 | 189 | func BenchmarkGet8(b *testing.B) { benchmarkGet(b, 8) } 190 | func BenchmarkGet32(b *testing.B) { benchmarkGet(b, 32) } 191 | func BenchmarkGet128(b *testing.B) { benchmarkGet(b, 128) } 192 | func BenchmarkGet512(b *testing.B) { benchmarkGet(b, 512) } 193 | 194 | func benchmarkGet(b *testing.B, shards int) { 195 | 196 | picker := peer.NewPicker(peer.Options{Replicas: 50}) 197 | 198 | var buckets []string 199 | for i := 0; i < shards; i++ { 200 | buckets = append(buckets, fmt.Sprintf("shard-%d", i)) 201 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: fmt.Sprintf("shard-%d", i)}}) 202 | } 203 | 204 | b.ResetTimer() 205 | 206 | for i := 0; i < b.N; i++ { 207 | picker.Get(buckets[i&(shards-1)]) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /instance.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright 2024 Derrick J. Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Package groupcache provides a data loading mechanism with caching 19 | // and de-duplication that works across a set of peer processes. 20 | // 21 | // Each Get() call first consults its local cache, otherwise delegates 22 | // to the requested key's canonical owner, which then checks its cache 23 | // or finally gets the data. In the common case, many concurrent 24 | // cache misses across a set of peers for the same key result in just 25 | // one cache fill. 26 | package groupcache 27 | 28 | import ( 29 | "context" 30 | "errors" 31 | "fmt" 32 | "sync" 33 | 34 | "github.com/groupcache/groupcache-go/v3/internal/singleflight" 35 | "github.com/groupcache/groupcache-go/v3/transport" 36 | "github.com/groupcache/groupcache-go/v3/transport/peer" 37 | ) 38 | 39 | type Logger interface { 40 | Info(msg string, args ...any) 41 | Error(msg string, args ...any) 42 | } 43 | 44 | // Options is the configuration for this instance of groupcache 45 | type Options struct { 46 | // HashFn is a function type that is used to calculate a hash used in the hash ring 47 | // Default is fnv1.HashBytes64 48 | HashFn peer.HashFn 49 | 50 | // Replicas is the number of replicas that will be used in the hash ring 51 | // Default is 50 52 | Replicas int 53 | 54 | // Logger is the logger that will be used by groupcache 55 | // Default is slog.Default() 56 | Logger Logger 57 | 58 | // Transport is the transport groupcache will use to communicate with peers in the cluster 59 | // Default is transport.HttpTransport 60 | Transport transport.Transport 61 | 62 | // CacheFactory returns a new instance of Cache which will be used by groupcache for both 63 | // the main and hot cache. 64 | CacheFactory func(maxBytes int64) (Cache, error) 65 | 66 | // MetricProvider (Optional) provides OpenTelemetry metrics integration for groupcache. 67 | MetricProvider *MeterProvider 68 | } 69 | 70 | // Instance of groupcache 71 | type Instance struct { 72 | groups map[string]*group 73 | mu sync.RWMutex 74 | picker *peer.Picker 75 | opts Options 76 | } 77 | 78 | // New instantiates a new Instance of groupcache with the provided options 79 | func New(opts Options) *Instance { 80 | if opts.CacheFactory == nil { 81 | opts.CacheFactory = func(maxBytes int64) (Cache, error) { 82 | return newMutexCache(maxBytes), nil 83 | } 84 | } 85 | 86 | if opts.Transport == nil { 87 | opts.Transport = transport.NewHttpTransport(transport.HttpTransportOptions{}) 88 | } 89 | 90 | i := &Instance{ 91 | groups: make(map[string]*group), 92 | opts: opts, 93 | } 94 | 95 | // Register our instance with the transport 96 | i.opts.Transport.Register(i) 97 | 98 | // Create a new peer picker using the provided opts 99 | i.picker = peer.NewPicker(peer.Options{ 100 | HashFn: opts.HashFn, 101 | Replicas: opts.Replicas, 102 | }) 103 | 104 | return i 105 | } 106 | 107 | // SetPeers is called by the list of peers changes 108 | func (i *Instance) SetPeers(ctx context.Context, peers []peer.Info) error { 109 | picker := peer.NewPicker(peer.Options{ 110 | HashFn: i.opts.HashFn, 111 | Replicas: i.opts.Replicas, 112 | }) 113 | 114 | // calls to Transport.NewClient() could block or take some time to create a new client 115 | // As such, we instantiate a new picker and prepare the clients before replacing the active 116 | // picker. 117 | var includesSelf bool 118 | for _, p := range peers { 119 | if p.IsSelf { 120 | picker.Add(&peer.NoOpClient{Info: peer.Info{Address: p.Address, IsSelf: true}}) 121 | includesSelf = true 122 | continue 123 | } 124 | client, err := i.opts.Transport.NewClient(ctx, p) 125 | if err != nil { 126 | return fmt.Errorf("during Transport.NewClient(): %w", err) 127 | } 128 | picker.Add(client) 129 | } 130 | 131 | if !includesSelf { 132 | return errors.New("peer.Info{IsSelf: true} missing; peer list must contain the address for this instance") 133 | } 134 | 135 | i.mu.Lock() 136 | i.picker = picker 137 | i.mu.Unlock() 138 | return nil 139 | } 140 | 141 | // PickPeer picks the peer for the provided key in a thread safe manner 142 | func (i *Instance) PickPeer(key string) (peer.Client, bool) { 143 | i.mu.RLock() 144 | defer i.mu.RUnlock() 145 | return i.picker.PickPeer(key) 146 | } 147 | 148 | // getAllPeers returns a list of clients for every peer in the cluster in a thread safe manner 149 | func (i *Instance) getAllPeers() []peer.Client { 150 | i.mu.RLock() 151 | defer i.mu.RUnlock() 152 | return i.picker.GetAll() 153 | } 154 | 155 | // NewGroup creates a coordinated group-aware Getter from a Getter. 156 | // 157 | // The returned Getter tries (but does not guarantee) to run only one 158 | // Get call at once for a given key across an entire set of peer 159 | // processes. Concurrent callers both in the local process and in 160 | // other processes receive copies of the answer once the original Get 161 | // completes. 162 | // 163 | // The group name must be unique for each getter. 164 | func (i *Instance) NewGroup(name string, cacheBytes int64, getter Getter) (Group, error) { 165 | if getter == nil { 166 | return nil, errors.New("NewGroup(): provided Getter cannot be nil") 167 | } 168 | 169 | i.mu.Lock() 170 | defer i.mu.Unlock() 171 | if _, dup := i.groups[name]; dup { 172 | return nil, fmt.Errorf("duplicate registration of group '%s'", name) 173 | } 174 | g := &group{ 175 | instance: i, 176 | name: name, 177 | getter: getter, 178 | maxCacheBytes: cacheBytes, 179 | loadGroup: &singleflight.Group{}, 180 | setGroup: &singleflight.Group{}, 181 | removeGroup: &singleflight.Group{}, 182 | } 183 | if err := g.ResetCacheSize(cacheBytes); err != nil { 184 | return nil, err 185 | } 186 | i.groups[name] = g 187 | 188 | // register metrics for this group if a MetricProvider was provided 189 | if i.opts.MetricProvider != nil { 190 | meter := i.opts.MetricProvider.getMeter() 191 | if err := g.registerInstruments(meter); err != nil { 192 | return nil, fmt.Errorf("failed to register metrics for group '%s': %w", name, err) 193 | } 194 | } 195 | 196 | return g, nil 197 | } 198 | 199 | // GetGroup returns the named group previously created with NewGroup, or 200 | // nil if there's no such group. 201 | func (i *Instance) GetGroup(name string) transport.Group { 202 | i.mu.RLock() 203 | g := i.groups[name] 204 | i.mu.RUnlock() 205 | return g 206 | } 207 | 208 | // RemoveGroup removes group from group pool 209 | func (i *Instance) RemoveGroup(name string) { 210 | i.mu.Lock() 211 | delete(i.groups, name) 212 | i.mu.Unlock() 213 | } 214 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | package groupcache 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/metric" 13 | "go.opentelemetry.io/otel/metric/noop" 14 | 15 | "github.com/groupcache/groupcache-go/v3/transport" 16 | ) 17 | 18 | func TestNewMeterProviderUsesGlobalProviderByDefault(t *testing.T) { 19 | t.Parallel() 20 | 21 | original := otel.GetMeterProvider() 22 | global := &recordingMeterProvider{} 23 | otel.SetMeterProvider(global) 24 | defer otel.SetMeterProvider(original) 25 | 26 | mp := NewMeterProvider() 27 | 28 | require.Equal(t, global, mp.underlying) 29 | assert.Contains(t, global.requested, instrumentationName) 30 | assert.Same(t, global.meter, mp.getMeter()) 31 | } 32 | 33 | func TestNewMeterProviderRespectsOverride(t *testing.T) { 34 | t.Parallel() 35 | 36 | original := otel.GetMeterProvider() 37 | defer otel.SetMeterProvider(original) 38 | 39 | global := &recordingMeterProvider{} 40 | otel.SetMeterProvider(global) 41 | initialGlobalRequests := len(global.requested) 42 | 43 | custom := &recordingMeterProvider{} 44 | mp := NewMeterProvider(WithMeterProvider(custom)) 45 | 46 | require.Equal(t, custom, mp.underlying) 47 | assert.Contains(t, custom.requested, instrumentationName) 48 | assert.Equal(t, initialGlobalRequests, len(global.requested), "global provider should not be invoked when override is provided") 49 | assert.Same(t, custom.meter, mp.getMeter()) 50 | } 51 | 52 | func TestNewGroupInstrumentsRegistersAllCounters(t *testing.T) { 53 | t.Parallel() 54 | 55 | meter := &recordingMeter{} 56 | 57 | inst, err := newGroupInstruments(meter) 58 | require.NoError(t, err) 59 | require.NotNil(t, inst) 60 | 61 | expectedCounters := []string{ 62 | "groupcache.group.gets", 63 | "groupcache.group.cache_hits", 64 | "groupcache.group.peer.loads", 65 | "groupcache.group.peer.errors", 66 | "groupcache.group.loads", 67 | "groupcache.group.loads.deduped", 68 | "groupcache.group.local.loads", 69 | "groupcache.group.local.load_errors", 70 | } 71 | assert.Equal(t, expectedCounters, meter.counterNames) 72 | assert.Equal(t, []string{"groupcache.group.peer.latency_max_ms"}, meter.updownNames) 73 | 74 | assert.NotNil(t, inst.GetsCounter()) 75 | assert.NotNil(t, inst.HitsCounter()) 76 | assert.NotNil(t, inst.PeerLoadsCounter()) 77 | assert.NotNil(t, inst.PeerErrorsCounter()) 78 | assert.NotNil(t, inst.LoadsCounter()) 79 | assert.NotNil(t, inst.LoadsDedupedCounter()) 80 | assert.NotNil(t, inst.LocalLoadsCounter()) 81 | assert.NotNil(t, inst.LocalLoadErrsCounter()) 82 | assert.NotNil(t, inst.GetFromPeersLatencyMaxGauge()) 83 | } 84 | 85 | func TestNewGroupInstrumentsErrorsOnCounterFailure(t *testing.T) { 86 | t.Parallel() 87 | 88 | expectedErr := errors.New("counter fail") 89 | meter := &failingObservableMeter{counterErr: expectedErr} 90 | 91 | inst, err := newGroupInstruments(meter) 92 | require.ErrorIs(t, err, expectedErr) 93 | assert.Nil(t, inst) 94 | } 95 | 96 | func TestNewGroupInstrumentsErrorsOnUpDownCounterFailure(t *testing.T) { 97 | t.Parallel() 98 | 99 | expectedErr := errors.New("updown fail") 100 | meter := &failingObservableMeter{upDownErr: expectedErr} 101 | 102 | inst, err := newGroupInstruments(meter) 103 | require.ErrorIs(t, err, expectedErr) 104 | assert.Nil(t, inst) 105 | } 106 | 107 | func TestNewCacheInstrumentsErrorsOnCounterFailure(t *testing.T) { 108 | t.Parallel() 109 | 110 | expectedErr := errors.New("counter fail") 111 | meter := &failingSyncMeter{counterErr: expectedErr} 112 | 113 | inst, err := newCacheInstruments(meter) 114 | require.ErrorIs(t, err, expectedErr) 115 | assert.Nil(t, inst) 116 | } 117 | 118 | func TestNewCacheInstrumentsErrorsOnUpDownCounterFailure(t *testing.T) { 119 | t.Parallel() 120 | 121 | expectedErr := errors.New("updown fail") 122 | meter := &failingSyncMeter{upDownErr: expectedErr} 123 | 124 | inst, err := newCacheInstruments(meter) 125 | require.ErrorIs(t, err, expectedErr) 126 | assert.Nil(t, inst) 127 | } 128 | 129 | func TestNewGroupPropagatesMetricRegistrationError(t *testing.T) { 130 | t.Parallel() 131 | 132 | expectedErr := errors.New("register fail") 133 | failMeter := &failingObservableMeter{counterErr: expectedErr} 134 | mp := NewMeterProvider(WithMeterProvider(&staticMeterProvider{meter: failMeter})) 135 | 136 | instance := New(Options{MetricProvider: mp}) 137 | 138 | g, err := instance.NewGroup("metrics-error", 1<<10, GetterFunc(func(_ context.Context, key string, dest transport.Sink) error { 139 | return dest.SetString("ok", time.Time{}) 140 | })) 141 | 142 | require.ErrorIs(t, err, expectedErr) 143 | assert.Nil(t, g) 144 | } 145 | 146 | func TestNewCacheInstrumentsRegistersAllCounters(t *testing.T) { 147 | t.Parallel() 148 | 149 | meter := &recordingSyncMeter{} 150 | 151 | inst, err := newCacheInstruments(meter) 152 | require.NoError(t, err) 153 | require.NotNil(t, inst) 154 | 155 | expectedCounters := []string{ 156 | "groupcache.cache.rejected", 157 | "groupcache.cache.gets", 158 | "groupcache.cache.hits", 159 | "groupcache.cache.evictions", 160 | } 161 | assert.Equal(t, expectedCounters, meter.counterNames) 162 | assert.Equal(t, []string{ 163 | "groupcache.cache.bytes", 164 | "groupcache.cache.items", 165 | }, meter.updownNames) 166 | 167 | assert.NotNil(t, inst.RejectedCounter()) 168 | assert.NotNil(t, inst.BytesGauge()) 169 | assert.NotNil(t, inst.ItemsGauge()) 170 | assert.NotNil(t, inst.GetsCounter()) 171 | assert.NotNil(t, inst.HitsCounter()) 172 | assert.NotNil(t, inst.EvictionsCounter()) 173 | } 174 | 175 | type recordingMeterProvider struct { 176 | noop.MeterProvider 177 | 178 | requested []string 179 | meter *recordingMeter 180 | } 181 | 182 | func (p *recordingMeterProvider) Meter(name string, _ ...metric.MeterOption) metric.Meter { 183 | p.requested = append(p.requested, name) 184 | if p.meter == nil { 185 | p.meter = &recordingMeter{} 186 | } 187 | return p.meter 188 | } 189 | 190 | type recordingMeter struct { 191 | noop.Meter 192 | 193 | counterNames []string 194 | updownNames []string 195 | } 196 | 197 | func (m *recordingMeter) Int64ObservableCounter(name string, _ ...metric.Int64ObservableCounterOption) (metric.Int64ObservableCounter, error) { 198 | m.counterNames = append(m.counterNames, name) 199 | return noop.Int64ObservableCounter{}, nil 200 | } 201 | 202 | func (m *recordingMeter) Int64ObservableUpDownCounter(name string, _ ...metric.Int64ObservableUpDownCounterOption) (metric.Int64ObservableUpDownCounter, error) { 203 | m.updownNames = append(m.updownNames, name) 204 | return noop.Int64ObservableUpDownCounter{}, nil 205 | } 206 | 207 | type failingObservableMeter struct { 208 | noop.Meter 209 | 210 | counterErr error 211 | upDownErr error 212 | } 213 | 214 | func (m *failingObservableMeter) Int64ObservableCounter(string, ...metric.Int64ObservableCounterOption) (metric.Int64ObservableCounter, error) { 215 | if m.counterErr != nil { 216 | return nil, m.counterErr 217 | } 218 | return noop.Int64ObservableCounter{}, nil 219 | } 220 | 221 | func (m *failingObservableMeter) Int64ObservableUpDownCounter(string, ...metric.Int64ObservableUpDownCounterOption) (metric.Int64ObservableUpDownCounter, error) { 222 | if m.upDownErr != nil { 223 | return nil, m.upDownErr 224 | } 225 | return noop.Int64ObservableUpDownCounter{}, nil 226 | } 227 | 228 | type failingSyncMeter struct { 229 | noop.Meter 230 | 231 | counterErr error 232 | upDownErr error 233 | } 234 | 235 | func (m *failingSyncMeter) Int64Counter(string, ...metric.Int64CounterOption) (metric.Int64Counter, error) { 236 | if m.counterErr != nil { 237 | return nil, m.counterErr 238 | } 239 | return noop.Int64Counter{}, nil 240 | } 241 | 242 | func (m *failingSyncMeter) Int64UpDownCounter(string, ...metric.Int64UpDownCounterOption) (metric.Int64UpDownCounter, error) { 243 | if m.upDownErr != nil { 244 | return nil, m.upDownErr 245 | } 246 | return noop.Int64UpDownCounter{}, nil 247 | } 248 | 249 | type staticMeterProvider struct { 250 | noop.MeterProvider 251 | meter metric.Meter 252 | } 253 | 254 | func (s *staticMeterProvider) Meter(string, ...metric.MeterOption) metric.Meter { 255 | return s.meter 256 | } 257 | 258 | type recordingSyncMeter struct { 259 | noop.Meter 260 | 261 | counterNames []string 262 | updownNames []string 263 | } 264 | 265 | func (m *recordingSyncMeter) Int64Counter(name string, _ ...metric.Int64CounterOption) (metric.Int64Counter, error) { 266 | m.counterNames = append(m.counterNames, name) 267 | return noop.Int64Counter{}, nil 268 | } 269 | 270 | func (m *recordingSyncMeter) Int64UpDownCounter(name string, _ ...metric.Int64UpDownCounterOption) (metric.Int64UpDownCounter, error) { 271 | m.updownNames = append(m.updownNames, name) 272 | return noop.Int64UpDownCounter{}, nil 273 | } 274 | -------------------------------------------------------------------------------- /transport/sinks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transport 18 | 19 | import ( 20 | "errors" 21 | "time" 22 | 23 | "google.golang.org/protobuf/proto" 24 | ) 25 | 26 | var _ Sink = &stringSink{} 27 | var _ Sink = &allocBytesSink{} 28 | var _ Sink = &protoSink{} 29 | var _ Sink = &truncBytesSink{} 30 | var _ Sink = &byteViewSink{} 31 | 32 | // A Sink receives data from a Get call. 33 | // 34 | // Implementation of Getter must call exactly one of the Set methods 35 | // on success. 36 | // 37 | // `e` sets an optional time in the future when the value will expire. 38 | // If you don't want expiration, pass the zero value for 39 | // `time.Time` (for instance, `time.Time{}`). 40 | type Sink interface { 41 | // SetString sets the value to s. 42 | SetString(s string, e time.Time) error 43 | 44 | // SetBytes sets the value to the contents of v. 45 | // The caller retains ownership of v. 46 | SetBytes(v []byte, e time.Time) error 47 | 48 | // SetProto sets the value to the encoded version of m. 49 | // The caller retains ownership of m. 50 | SetProto(m proto.Message, e time.Time) error 51 | 52 | // View returns a frozen view of the bytes for caching. 53 | View() (ByteView, error) 54 | } 55 | 56 | func cloneBytes(b []byte) []byte { 57 | c := make([]byte, len(b)) 58 | copy(c, b) 59 | return c 60 | } 61 | 62 | func SetSinkView(s Sink, v ByteView) error { 63 | // A viewSetter is a Sink that can also receive its value from 64 | // a ByteView. This is a fast path to minimize copies when the 65 | // item was already cached locally in memory (where it's 66 | // cached as a ByteView) 67 | type viewSetter interface { 68 | setView(v ByteView) error 69 | } 70 | if vs, ok := s.(viewSetter); ok { 71 | return vs.setView(v) 72 | } 73 | if v.b != nil { 74 | return s.SetBytes(v.b, v.Expire()) 75 | } 76 | return s.SetString(v.s, v.Expire()) 77 | } 78 | 79 | // StringSink returns a Sink that populates the provided string pointer. 80 | func StringSink(sp *string) Sink { 81 | return &stringSink{sp: sp} 82 | } 83 | 84 | type stringSink struct { 85 | sp *string 86 | v ByteView 87 | } 88 | 89 | func (s *stringSink) View() (ByteView, error) { 90 | return s.v, nil 91 | } 92 | 93 | func (s *stringSink) SetString(v string, e time.Time) error { 94 | s.v.b = nil 95 | s.v.s = v 96 | *s.sp = v 97 | s.v.e = e 98 | return nil 99 | } 100 | 101 | func (s *stringSink) SetBytes(v []byte, e time.Time) error { 102 | return s.SetString(string(v), e) 103 | } 104 | 105 | func (s *stringSink) SetProto(m proto.Message, e time.Time) error { 106 | b, err := proto.Marshal(m) 107 | if err != nil { 108 | return err 109 | } 110 | s.v.b = b 111 | *s.sp = string(b) 112 | s.v.e = e 113 | return nil 114 | } 115 | 116 | // ByteViewSink returns a Sink that populates a ByteView. 117 | func ByteViewSink(dst *ByteView) Sink { 118 | if dst == nil { 119 | panic("nil dst") 120 | } 121 | return &byteViewSink{dst: dst} 122 | } 123 | 124 | type byteViewSink struct { 125 | dst *ByteView 126 | 127 | // if this code ever ends up tracking that at least one set* 128 | // method was called, don't make it an error to call set 129 | // methods multiple times. Lorry's payload.go does that, and 130 | // it makes sense. The comment at the top of this file about 131 | // "exactly one of the Set methods" is overly strict. We 132 | // really care about at least once (in a handler), but if 133 | // multiple handlers fail (or multiple functions in a program 134 | // using a Sink), it's okay to re-use the same one. 135 | } 136 | 137 | func (s *byteViewSink) setView(v ByteView) error { 138 | *s.dst = v 139 | return nil 140 | } 141 | 142 | func (s *byteViewSink) View() (ByteView, error) { 143 | return *s.dst, nil 144 | } 145 | 146 | func (s *byteViewSink) SetProto(m proto.Message, e time.Time) error { 147 | b, err := proto.Marshal(m) 148 | if err != nil { 149 | return err 150 | } 151 | *s.dst = ByteView{b: b, e: e} 152 | return nil 153 | } 154 | 155 | func (s *byteViewSink) SetBytes(b []byte, e time.Time) error { 156 | *s.dst = ByteView{b: cloneBytes(b), e: e} 157 | return nil 158 | } 159 | 160 | func (s *byteViewSink) SetString(v string, e time.Time) error { 161 | *s.dst = ByteView{s: v, e: e} 162 | return nil 163 | } 164 | 165 | // ProtoSink returns a sink that unmarshals binary proto values into m. 166 | func ProtoSink(m proto.Message) Sink { 167 | return &protoSink{ 168 | dst: m, 169 | } 170 | } 171 | 172 | type protoSink struct { 173 | dst proto.Message // authoritative value 174 | 175 | v ByteView // encoded 176 | } 177 | 178 | func (s *protoSink) View() (ByteView, error) { 179 | return s.v, nil 180 | } 181 | 182 | func (s *protoSink) SetBytes(b []byte, e time.Time) error { 183 | err := proto.Unmarshal(b, s.dst) 184 | if err != nil { 185 | return err 186 | } 187 | s.v.b = cloneBytes(b) 188 | s.v.s = "" 189 | s.v.e = e 190 | return nil 191 | } 192 | 193 | func (s *protoSink) SetString(v string, e time.Time) error { 194 | b := []byte(v) 195 | err := proto.Unmarshal(b, s.dst) 196 | if err != nil { 197 | return err 198 | } 199 | s.v.b = b 200 | s.v.s = "" 201 | s.v.e = e 202 | return nil 203 | } 204 | 205 | func (s *protoSink) SetProto(m proto.Message, e time.Time) error { 206 | b, err := proto.Marshal(m) 207 | if err != nil { 208 | return err 209 | } 210 | // TODO(bradfitz): optimize for same-task case more and write 211 | // right through? would need to document ownership rules at 212 | // the same time. but then we could just assign *dst = *m 213 | // here. This works for now: 214 | err = proto.Unmarshal(b, s.dst) 215 | if err != nil { 216 | return err 217 | } 218 | s.v.b = b 219 | s.v.s = "" 220 | s.v.e = e 221 | return nil 222 | } 223 | 224 | // AllocatingByteSliceSink returns a Sink that allocates 225 | // a byte slice to hold the received value and assigns 226 | // it to *dst. The memory is not retained by groupcache. 227 | func AllocatingByteSliceSink(dst *[]byte) Sink { 228 | return &allocBytesSink{dst: dst} 229 | } 230 | 231 | type allocBytesSink struct { 232 | dst *[]byte 233 | v ByteView 234 | } 235 | 236 | func (s *allocBytesSink) View() (ByteView, error) { 237 | return s.v, nil 238 | } 239 | 240 | func (s *allocBytesSink) setView(v ByteView) error { 241 | if v.b != nil { 242 | *s.dst = cloneBytes(v.b) 243 | } else { 244 | *s.dst = []byte(v.s) 245 | } 246 | s.v = v 247 | return nil 248 | } 249 | 250 | func (s *allocBytesSink) SetProto(m proto.Message, e time.Time) error { 251 | b, err := proto.Marshal(m) 252 | if err != nil { 253 | return err 254 | } 255 | return s.setBytesOwned(b, e) 256 | } 257 | 258 | func (s *allocBytesSink) SetBytes(b []byte, e time.Time) error { 259 | return s.setBytesOwned(cloneBytes(b), e) 260 | } 261 | 262 | func (s *allocBytesSink) setBytesOwned(b []byte, e time.Time) error { 263 | if s.dst == nil { 264 | return errors.New("nil AllocatingByteSliceSink *[]byte dst") 265 | } 266 | *s.dst = cloneBytes(b) // another copy, protecting the read-only s.v.b view 267 | s.v.b = b 268 | s.v.s = "" 269 | s.v.e = e 270 | return nil 271 | } 272 | 273 | func (s *allocBytesSink) SetString(v string, e time.Time) error { 274 | if s.dst == nil { 275 | return errors.New("nil AllocatingByteSliceSink *[]byte dst") 276 | } 277 | *s.dst = []byte(v) 278 | s.v.b = nil 279 | s.v.s = v 280 | s.v.e = e 281 | return nil 282 | } 283 | 284 | // TruncatingByteSliceSink returns a Sink that writes up to len(*dst) 285 | // bytes to *dst. If more bytes are available, they're silently 286 | // truncated. If fewer bytes are available than len(*dst), *dst 287 | // is shrunk to fit the number of bytes available. 288 | func TruncatingByteSliceSink(dst *[]byte) Sink { 289 | return &truncBytesSink{dst: dst} 290 | } 291 | 292 | type truncBytesSink struct { 293 | dst *[]byte 294 | v ByteView 295 | } 296 | 297 | func (s *truncBytesSink) View() (ByteView, error) { 298 | return s.v, nil 299 | } 300 | 301 | func (s *truncBytesSink) SetProto(m proto.Message, e time.Time) error { 302 | b, err := proto.Marshal(m) 303 | if err != nil { 304 | return err 305 | } 306 | return s.setBytesOwned(b, e) 307 | } 308 | 309 | func (s *truncBytesSink) SetBytes(b []byte, e time.Time) error { 310 | return s.setBytesOwned(cloneBytes(b), e) 311 | } 312 | 313 | func (s *truncBytesSink) setBytesOwned(b []byte, e time.Time) error { 314 | if s.dst == nil { 315 | return errors.New("nil TruncatingByteSliceSink *[]byte dst") 316 | } 317 | n := copy(*s.dst, b) 318 | if n < len(*s.dst) { 319 | *s.dst = (*s.dst)[:n] 320 | } 321 | s.v.b = b 322 | s.v.s = "" 323 | s.v.e = e 324 | return nil 325 | } 326 | 327 | func (s *truncBytesSink) SetString(v string, e time.Time) error { 328 | if s.dst == nil { 329 | return errors.New("nil TruncatingByteSliceSink *[]byte dst") 330 | } 331 | n := copy(*s.dst, v) 332 | if n < len(*s.dst) { 333 | *s.dst = (*s.dst)[:n] 334 | } 335 | s.v.b = nil 336 | s.v.s = v 337 | s.v.e = e 338 | return nil 339 | } 340 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright Derrick J Wippler 4 | Copyright 2025 Arsene Tochemey Gandote 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package groupcache 20 | 21 | import ( 22 | "strconv" 23 | "sync/atomic" 24 | 25 | "go.opentelemetry.io/otel" 26 | "go.opentelemetry.io/otel/metric" 27 | ) 28 | 29 | const ( 30 | instrumentationName = "github.com/groupcache/groupcache-go/instrumentation/otel" 31 | ) 32 | 33 | // An AtomicInt is an int64 to be accessed atomically. 34 | type AtomicInt int64 35 | 36 | // Add atomically adds n to i. 37 | func (i *AtomicInt) Add(n int64) { 38 | atomic.AddInt64((*int64)(i), n) 39 | } 40 | 41 | // Store atomically stores n to i. 42 | func (i *AtomicInt) Store(n int64) { 43 | atomic.StoreInt64((*int64)(i), n) 44 | } 45 | 46 | // Get atomically gets the value of i. 47 | func (i *AtomicInt) Get() int64 { 48 | return atomic.LoadInt64((*int64)(i)) 49 | } 50 | 51 | func (i *AtomicInt) String() string { 52 | return strconv.FormatInt(i.Get(), 10) 53 | } 54 | 55 | // CacheStats are returned by stats accessors on Group. 56 | type CacheStats struct { 57 | // Rejected is a counter of the total number of items that were not added to 58 | // the cache due to some consideration of the underlying cache implementation. 59 | Rejected int64 60 | // Bytes is a gauge of how many bytes are in the cache 61 | Bytes int64 62 | // Items is a gauge of how many items are in the cache 63 | Items int64 64 | // Gets reports the total get requests 65 | Gets int64 66 | // Hits reports the total successful cache hits 67 | Hits int64 68 | // Evictions reports the total number of evictions 69 | Evictions int64 70 | } 71 | 72 | // GroupStats are per-group statistics. 73 | type GroupStats struct { 74 | Gets AtomicInt // any Get request, including from peers 75 | CacheHits AtomicInt // either cache was good 76 | GetFromPeersLatencyLower AtomicInt // slowest duration to request value from peers 77 | PeerLoads AtomicInt // either remote load or remote cache hit (not an error) 78 | PeerErrors AtomicInt 79 | Loads AtomicInt // (gets - cacheHits) 80 | LoadsDeduped AtomicInt // after singleflight 81 | LocalLoads AtomicInt // total good local loads 82 | LocalLoadErrs AtomicInt // total bad local loads 83 | } 84 | 85 | type MeterProviderOption func(*MeterProvider) 86 | 87 | func WithMeterProvider(mp metric.MeterProvider) MeterProviderOption { 88 | return func(m *MeterProvider) { 89 | if mp != nil { 90 | m.underlying = mp 91 | } 92 | } 93 | } 94 | 95 | type MeterProvider struct { 96 | underlying metric.MeterProvider 97 | meter metric.Meter 98 | } 99 | 100 | func NewMeterProvider(opts ...MeterProviderOption) *MeterProvider { 101 | mp := &MeterProvider{ 102 | underlying: otel.GetMeterProvider(), 103 | } 104 | 105 | for _, opt := range opts { 106 | opt(mp) 107 | } 108 | 109 | mp.meter = mp.underlying.Meter(instrumentationName) 110 | return mp 111 | } 112 | 113 | func (mp *MeterProvider) getMeter() metric.Meter { 114 | return mp.meter 115 | } 116 | 117 | type groupInstruments struct { 118 | getsCounter metric.Int64ObservableCounter 119 | hitsCounter metric.Int64ObservableCounter 120 | peerLoadsCounter metric.Int64ObservableCounter 121 | peerErrorsCounter metric.Int64ObservableCounter 122 | loadsCounter metric.Int64ObservableCounter 123 | loadsDedupedCounter metric.Int64ObservableCounter 124 | localLoadsCounter metric.Int64ObservableCounter 125 | localLoadErrsCounter metric.Int64ObservableCounter 126 | getFromPeersLatencyMaxGauge metric.Int64ObservableUpDownCounter 127 | } 128 | 129 | // newGroupInstruments registers all instruments that map to GroupStats counters. 130 | func newGroupInstruments(meter metric.Meter) (*groupInstruments, error) { 131 | getsCounter, err := meter.Int64ObservableCounter( 132 | "groupcache.group.gets", 133 | metric.WithDescription("Total get requests for the group"), 134 | ) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | hitsCounter, err := meter.Int64ObservableCounter( 140 | "groupcache.group.cache_hits", 141 | metric.WithDescription("Total successful cache hits"), 142 | ) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | peerLoadsCounter, err := meter.Int64ObservableCounter( 148 | "groupcache.group.peer.loads", 149 | metric.WithDescription("Total successful loads from peers"), 150 | ) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | peerErrorsCounter, err := meter.Int64ObservableCounter( 156 | "groupcache.group.peer.errors", 157 | metric.WithDescription("Total failed loads from peers"), 158 | ) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | loadsCounter, err := meter.Int64ObservableCounter( 164 | "groupcache.group.loads", 165 | metric.WithDescription("Total loads after cache miss"), 166 | ) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | loadsDedupedCounter, err := meter.Int64ObservableCounter( 172 | "groupcache.group.loads.deduped", 173 | metric.WithDescription("Total loads de-duplicated by singleflight"), 174 | ) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | localLoadsCounter, err := meter.Int64ObservableCounter( 180 | "groupcache.group.local.loads", 181 | metric.WithDescription("Total successful local loads"), 182 | ) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | localLoadErrsCounter, err := meter.Int64ObservableCounter( 188 | "groupcache.group.local.load_errors", 189 | metric.WithDescription("Total failed local loads"), 190 | ) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | getFromPeersLatencyMaxGauge, err := meter.Int64ObservableUpDownCounter( 196 | "groupcache.group.peer.latency_max_ms", 197 | metric.WithDescription("Maximum latency (ms) observed when loading from peers"), 198 | ) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | return &groupInstruments{ 204 | getsCounter: getsCounter, 205 | hitsCounter: hitsCounter, 206 | peerLoadsCounter: peerLoadsCounter, 207 | peerErrorsCounter: peerErrorsCounter, 208 | loadsCounter: loadsCounter, 209 | loadsDedupedCounter: loadsDedupedCounter, 210 | localLoadsCounter: localLoadsCounter, 211 | localLoadErrsCounter: localLoadErrsCounter, 212 | getFromPeersLatencyMaxGauge: getFromPeersLatencyMaxGauge, 213 | }, nil 214 | } 215 | 216 | func (gm *groupInstruments) GetsCounter() metric.Int64ObservableCounter { 217 | return gm.getsCounter 218 | } 219 | 220 | func (gm *groupInstruments) HitsCounter() metric.Int64ObservableCounter { 221 | return gm.hitsCounter 222 | } 223 | 224 | func (gm *groupInstruments) PeerLoadsCounter() metric.Int64ObservableCounter { 225 | return gm.peerLoadsCounter 226 | } 227 | 228 | func (gm *groupInstruments) PeerErrorsCounter() metric.Int64ObservableCounter { 229 | return gm.peerErrorsCounter 230 | } 231 | 232 | func (gm *groupInstruments) LoadsCounter() metric.Int64ObservableCounter { 233 | return gm.loadsCounter 234 | } 235 | 236 | func (gm *groupInstruments) LoadsDedupedCounter() metric.Int64ObservableCounter { 237 | return gm.loadsDedupedCounter 238 | } 239 | 240 | func (gm *groupInstruments) LocalLoadsCounter() metric.Int64ObservableCounter { 241 | return gm.localLoadsCounter 242 | } 243 | 244 | func (gm *groupInstruments) LocalLoadErrsCounter() metric.Int64ObservableCounter { 245 | return gm.localLoadErrsCounter 246 | } 247 | 248 | func (gm *groupInstruments) GetFromPeersLatencyMaxGauge() metric.Int64ObservableUpDownCounter { 249 | return gm.getFromPeersLatencyMaxGauge 250 | } 251 | 252 | type cacheInstruments struct { 253 | rejectedCounter metric.Int64Counter 254 | bytesGauge metric.Int64UpDownCounter 255 | itemsGauge metric.Int64UpDownCounter 256 | getsCounter metric.Int64Counter 257 | hitsCounter metric.Int64Counter 258 | evictionsCounter metric.Int64Counter 259 | } 260 | 261 | func newCacheInstruments(meter metric.Meter) (*cacheInstruments, error) { 262 | rejectedCounter, err := meter.Int64Counter("groupcache.cache.rejected", 263 | metric.WithDescription("Total number of items rejected from cache"), 264 | ) 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | bytesGauge, err := meter.Int64UpDownCounter( 270 | "groupcache.cache.bytes", 271 | metric.WithDescription("Number of bytes in cache"), 272 | ) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | itemsGauge, err := meter.Int64UpDownCounter( 278 | "groupcache.cache.items", 279 | metric.WithDescription("Number of items in cache"), 280 | ) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | getsCounter, err := meter.Int64Counter( 286 | "groupcache.cache.gets", 287 | metric.WithDescription("Total get requests"), 288 | ) 289 | if err != nil { 290 | return nil, err 291 | } 292 | 293 | hitsCounter, err := meter.Int64Counter( 294 | "groupcache.cache.hits", 295 | metric.WithDescription("Total successful cache hits"), 296 | ) 297 | if err != nil { 298 | return nil, err 299 | } 300 | 301 | evictionsCounter, err := meter.Int64Counter( 302 | "groupcache.cache.evictions", 303 | metric.WithDescription("Total number of evictions"), 304 | ) 305 | if err != nil { 306 | return nil, err 307 | } 308 | 309 | return &cacheInstruments{ 310 | rejectedCounter: rejectedCounter, 311 | bytesGauge: bytesGauge, 312 | itemsGauge: itemsGauge, 313 | getsCounter: getsCounter, 314 | hitsCounter: hitsCounter, 315 | evictionsCounter: evictionsCounter, 316 | }, nil 317 | } 318 | 319 | func (cm *cacheInstruments) RejectedCounter() metric.Int64Counter { 320 | return cm.rejectedCounter 321 | } 322 | 323 | func (cm *cacheInstruments) BytesGauge() metric.Int64UpDownCounter { 324 | return cm.bytesGauge 325 | } 326 | 327 | func (cm *cacheInstruments) ItemsGauge() metric.Int64UpDownCounter { 328 | return cm.itemsGauge 329 | } 330 | 331 | func (cm *cacheInstruments) GetsCounter() metric.Int64Counter { 332 | return cm.getsCounter 333 | } 334 | 335 | func (cm *cacheInstruments) HitsCounter() metric.Int64Counter { 336 | return cm.hitsCounter 337 | } 338 | 339 | func (cm *cacheInstruments) EvictionsCounter() metric.Int64Counter { 340 | return cm.evictionsCounter 341 | } 342 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /transport/http_transport_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Derrick J Wippler 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transport_test 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io" 23 | "log/slog" 24 | "net/http" 25 | "net/http/httptest" 26 | "net/url" 27 | "strconv" 28 | "strings" 29 | "sync" 30 | "testing" 31 | "time" 32 | 33 | "github.com/stretchr/testify/assert" 34 | "github.com/stretchr/testify/require" 35 | "go.opentelemetry.io/otel/attribute" 36 | "go.opentelemetry.io/otel/codes" 37 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 38 | "go.opentelemetry.io/otel/sdk/trace/tracetest" 39 | "go.opentelemetry.io/otel/trace" 40 | "google.golang.org/protobuf/proto" 41 | 42 | "github.com/groupcache/groupcache-go/v3" 43 | "github.com/groupcache/groupcache-go/v3/cluster" 44 | "github.com/groupcache/groupcache-go/v3/transport" 45 | "github.com/groupcache/groupcache-go/v3/transport/pb" 46 | "github.com/groupcache/groupcache-go/v3/transport/peer" 47 | ) 48 | 49 | const groupName = "group-a" 50 | 51 | func TestHttpTransport(t *testing.T) { 52 | // Start a http server to count the number of non cached hits 53 | var serverHits int 54 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | _, _ = fmt.Fprintln(w, "Hello") 56 | serverHits++ 57 | })) 58 | defer ts.Close() 59 | 60 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 61 | defer cancel() 62 | 63 | // ListenAndServe a cluster of 4 groupcache instances with HTTP Transport 64 | require.NoError(t, cluster.Start(ctx, 4, groupcache.Options{ 65 | Transport: transport.NewHttpTransport(transport.HttpTransportOptions{ 66 | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 67 | }), 68 | })) 69 | defer func() { _ = cluster.Shutdown(context.Background()) }() 70 | 71 | // Create a group on each instance in the cluster 72 | for idx, d := range cluster.ListDaemons() { 73 | _, err := d.GetInstance().NewGroup(groupName, 1<<20, 74 | groupcache.GetterFunc(func(ctx context.Context, key string, dest transport.Sink) error { 75 | if _, err := http.Get(ts.URL); err != nil { 76 | t.Logf("HTTP request from getter failed with '%s'", err) 77 | } 78 | return dest.SetString(strconv.Itoa(idx)+":"+key, time.Time{}) 79 | })) 80 | require.NoError(t, err) 81 | } 82 | 83 | // Create new transport with default options 84 | tr := transport.NewHttpTransport(transport.HttpTransportOptions{}) 85 | 86 | // Create a new client to the first peer in the cluster 87 | c, err := tr.NewClient(ctx, cluster.PeerAt(0)) 88 | require.NoError(t, err) 89 | 90 | // Each new key should result in a new hit to the test server 91 | for _, key := range testKeys(100) { 92 | var resp pb.GetResponse 93 | require.NoError(t, getRequest(ctx, c, groupName, key, &resp)) 94 | 95 | // The value should be in the format `instance:key` 96 | assert.True(t, strings.HasSuffix(string(resp.Value), ":"+key)) 97 | } 98 | assert.Equal(t, 100, serverHits) 99 | 100 | serverHits = 0 101 | 102 | // Multiple gets on the same key to the owner of the key 103 | owner := cluster.FindOwningDaemon("new-key") 104 | for i := 0; i < 2; i++ { 105 | var resp pb.GetResponse 106 | require.NoError(t, getRequest(ctx, owner.MustClient(), groupName, "new-key", &resp)) 107 | } 108 | // Should result in only 1 server get 109 | assert.Equal(t, 1, serverHits) 110 | 111 | // Remove the key from the owner and we should see another server hit 112 | var resp pb.GetResponse 113 | require.NoError(t, removeRequest(ctx, owner.MustClient(), groupName, "new-key")) 114 | require.NoError(t, getRequest(ctx, owner.MustClient(), groupName, "new-key", &resp)) 115 | assert.Equal(t, 2, serverHits) 116 | 117 | // Remove the key, and set it with a different value 118 | require.NoError(t, removeRequest(ctx, owner.MustClient(), groupName, "new-key")) 119 | require.NoError(t, setRequest(ctx, owner.MustClient(), groupName, "new-key", "new-value")) 120 | 121 | require.NoError(t, getRequest(ctx, owner.MustClient(), groupName, "new-key", &resp)) 122 | assert.Equal(t, []byte("new-value"), resp.Value) 123 | // Should not see any new server hits 124 | assert.Equal(t, 2, serverHits) 125 | } 126 | 127 | func getRequest(ctx context.Context, c peer.Client, group, key string, resp *pb.GetResponse) error { 128 | req := &pb.GetRequest{ 129 | Group: &group, 130 | Key: &key, 131 | } 132 | return c.Get(ctx, req, resp) 133 | } 134 | 135 | func setRequest(ctx context.Context, c peer.Client, group, key, value string) error { 136 | req := &pb.SetRequest{ 137 | Value: []byte(value), 138 | Group: &group, 139 | Key: &key, 140 | } 141 | return c.Set(ctx, req) 142 | } 143 | 144 | func removeRequest(ctx context.Context, c peer.Client, group, key string) error { 145 | req := &pb.GetRequest{ 146 | Group: &group, 147 | Key: &key, 148 | } 149 | return c.Remove(ctx, req) 150 | } 151 | 152 | func testKeys(n int) (keys []string) { 153 | keys = make([]string, n) 154 | for i := range keys { 155 | keys[i] = strconv.Itoa(i) 156 | } 157 | return 158 | } 159 | 160 | // nolint 161 | func TestHttpTransportWithTracerEmitsServerSpan(t *testing.T) { 162 | recorder := tracetest.NewSpanRecorder() 163 | tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) 164 | 165 | tr := transport.NewHttpTransport(transport.HttpTransportOptions{ 166 | Tracer: transport.NewTracer(transport.WithTraceProvider(tracerProvider)), 167 | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 168 | }) 169 | 170 | instance := newTracingInstance() 171 | tr.Register(instance) 172 | 173 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 174 | defer cancel() 175 | 176 | require.NoError(t, tr.ListenAndServe(ctx, "127.0.0.1:0")) 177 | t.Cleanup(func() { _ = tr.Shutdown(context.Background()) }) 178 | 179 | resp, err := http.Get(fmt.Sprintf("http://%s%s%s/%s", tr.ListenAddress(), transport.DefaultBasePath, groupName, "tracer")) 180 | require.NoError(t, err) 181 | defer resp.Body.Close() 182 | require.Equal(t, http.StatusOK, resp.StatusCode) 183 | 184 | spans := recorder.Ended() 185 | require.NotEmpty(t, spans, "expected otelhttp handler to record a span when tracer is configured") 186 | 187 | var serverSpanFound bool 188 | for _, span := range spans { 189 | if span.SpanKind() == trace.SpanKindServer { 190 | serverSpanFound = true 191 | break 192 | } 193 | } 194 | assert.True(t, serverSpanFound, "expected a server span recorded by the tracer provider") 195 | } 196 | 197 | // nolint 198 | func TestHttpClientWithTracerRecordsSpans(t *testing.T) { 199 | recorder := tracetest.NewSpanRecorder() 200 | tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) 201 | tracer := transport.NewTracer( 202 | transport.WithTraceProvider(tracerProvider), 203 | transport.WithTracerAttributes(attribute.String("component", "http-client")), 204 | ) 205 | 206 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 207 | switch { 208 | case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "fail-get"): 209 | http.Error(w, "unavailable", http.StatusServiceUnavailable) 210 | case r.Method == http.MethodGet: 211 | body, _ := proto.Marshal(&pb.GetResponse{Value: []byte("value")}) 212 | _, _ = w.Write(body) 213 | case r.Method == http.MethodPut && strings.Contains(r.URL.Path, "fail-set"): 214 | http.Error(w, "cannot set", http.StatusInternalServerError) 215 | case r.Method == http.MethodPut: 216 | w.WriteHeader(http.StatusOK) 217 | case r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "fail-remove"): 218 | http.Error(w, "cannot delete", http.StatusInternalServerError) 219 | case r.Method == http.MethodDelete: 220 | w.WriteHeader(http.StatusOK) 221 | default: 222 | w.WriteHeader(http.StatusMethodNotAllowed) 223 | } 224 | })) 225 | defer server.Close() 226 | 227 | serverURL, err := url.Parse(server.URL) 228 | require.NoError(t, err) 229 | 230 | tr := transport.NewHttpTransport(transport.HttpTransportOptions{ 231 | Tracer: tracer, 232 | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 233 | }) 234 | 235 | client, err := tr.NewClient(context.Background(), peer.Info{Address: serverURL.Host}) 236 | require.NoError(t, err) 237 | 238 | ctx := context.Background() 239 | 240 | require.NoError(t, setRequest(ctx, client, groupName, "ok-set", "value")) 241 | 242 | var resp pb.GetResponse 243 | require.NoError(t, getRequest(ctx, client, groupName, "ok-get", &resp)) 244 | require.NoError(t, removeRequest(ctx, client, groupName, "ok-remove")) 245 | 246 | require.Error(t, getRequest(ctx, client, groupName, "fail-get", &resp)) 247 | require.Error(t, setRequest(ctx, client, groupName, "fail-set", "value")) 248 | require.Error(t, removeRequest(ctx, client, groupName, "fail-remove")) 249 | 250 | spans := recorder.Ended() 251 | clientSpans := spansWithPrefix(spans, "GroupCache.") 252 | require.Len(t, clientSpans, 6) 253 | 254 | expected := []struct { 255 | name string 256 | status codes.Code 257 | }{ 258 | {name: "GroupCache.Set", status: codes.Ok}, 259 | {name: "GroupCache.Get", status: codes.Ok}, 260 | {name: "GroupCache.Remove", status: codes.Ok}, 261 | {name: "GroupCache.Get", status: codes.Error}, 262 | {name: "GroupCache.Set", status: codes.Error}, 263 | {name: "GroupCache.Remove", status: codes.Error}, 264 | } 265 | 266 | for idx, exp := range expected { 267 | require.Less(t, idx, len(clientSpans)) 268 | assert.Equal(t, exp.name, clientSpans[idx].Name()) 269 | assert.Equal(t, exp.status, clientSpans[idx].Status().Code) 270 | assert.True(t, spanHasAttribute(clientSpans[idx], attribute.Key("component"), "http-client")) 271 | } 272 | } 273 | 274 | type tracingInstance struct { 275 | group *tracingGroup 276 | } 277 | 278 | func newTracingInstance() *tracingInstance { 279 | return &tracingInstance{group: newTracingGroup()} 280 | } 281 | 282 | func (t *tracingInstance) GetGroup(_ string) transport.Group { 283 | return t.group 284 | } 285 | 286 | type tracingGroup struct { 287 | values map[string][]byte 288 | mu sync.Mutex 289 | } 290 | 291 | func newTracingGroup() *tracingGroup { 292 | return &tracingGroup{ 293 | values: make(map[string][]byte), 294 | } 295 | } 296 | 297 | func (t *tracingGroup) Set(_ context.Context, key string, value []byte, _ time.Time, _ bool) error { 298 | t.mu.Lock() 299 | defer t.mu.Unlock() 300 | t.values[key] = append([]byte(nil), value...) 301 | return nil 302 | } 303 | 304 | func (t *tracingGroup) RemoteSet(key string, value []byte, expire time.Time) { 305 | _ = t.Set(context.Background(), key, value, expire, false) 306 | } 307 | 308 | func (t *tracingGroup) Get(_ context.Context, key string, dest transport.Sink) error { 309 | t.mu.Lock() 310 | value, ok := t.values[key] 311 | t.mu.Unlock() 312 | 313 | if !ok { 314 | value = []byte("value:" + key) 315 | } 316 | return dest.SetBytes(value, time.Time{}) 317 | } 318 | 319 | func (t *tracingGroup) Remove(_ context.Context, key string) error { 320 | t.LocalRemove(key) 321 | return nil 322 | } 323 | 324 | func (t *tracingGroup) LocalRemove(key string) { 325 | t.mu.Lock() 326 | defer t.mu.Unlock() 327 | delete(t.values, key) 328 | } 329 | 330 | func (t *tracingGroup) UsedBytes() (int64, int64) { 331 | return 0, 0 332 | } 333 | 334 | func (t *tracingGroup) Name() string { 335 | return groupName 336 | } 337 | 338 | func spanHasAttribute(span sdktrace.ReadOnlySpan, key attribute.Key, expected string) bool { 339 | for _, attr := range span.Attributes() { 340 | if attr.Key == key && attr.Value.AsString() == expected { 341 | return true 342 | } 343 | } 344 | return false 345 | } 346 | 347 | func spansWithPrefix(spans []sdktrace.ReadOnlySpan, prefix string) []sdktrace.ReadOnlySpan { 348 | filtered := make([]sdktrace.ReadOnlySpan, 0, len(spans)) 349 | for _, span := range spans { 350 | if strings.HasPrefix(span.Name(), prefix) { 351 | filtered = append(filtered, span) 352 | } 353 | } 354 | return filtered 355 | } 356 | -------------------------------------------------------------------------------- /transport/pb/groupcache.pb.go: -------------------------------------------------------------------------------- 1 | // 2 | //Copyright 2012 Google Inc. 3 | // 4 | //Licensed under the Apache License, Version 2.0 (the "License"); 5 | //you may not use this file except in compliance with the License. 6 | //You may obtain a copy of the License at 7 | // 8 | //http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | //Unless required by applicable law or agreed to in writing, software 11 | //distributed under the License is distributed on an "AS IS" BASIS, 12 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | //See the License for the specific language governing permissions and 14 | //limitations under the License. 15 | 16 | // Code generated by protoc-gen-go. DO NOT EDIT. 17 | // versions: 18 | // protoc-gen-go v1.32.0 19 | // protoc (unknown) 20 | // source: transport/pb/groupcache.proto 21 | 22 | package pb 23 | 24 | import ( 25 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 26 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 27 | reflect "reflect" 28 | sync "sync" 29 | ) 30 | 31 | const ( 32 | // Verify that this generated code is sufficiently up-to-date. 33 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 34 | // Verify that runtime/protoimpl is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 36 | ) 37 | 38 | type GetRequest struct { 39 | state protoimpl.MessageState 40 | sizeCache protoimpl.SizeCache 41 | unknownFields protoimpl.UnknownFields 42 | 43 | Group *string `protobuf:"bytes,1,req,name=group" json:"group,omitempty"` 44 | Key *string `protobuf:"bytes,2,req,name=key" json:"key,omitempty"` // not actually required/guaranteed to be UTF-8 45 | } 46 | 47 | func (x *GetRequest) Reset() { 48 | *x = GetRequest{} 49 | if protoimpl.UnsafeEnabled { 50 | mi := &file_transport_pb_groupcache_proto_msgTypes[0] 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | ms.StoreMessageInfo(mi) 53 | } 54 | } 55 | 56 | func (x *GetRequest) String() string { 57 | return protoimpl.X.MessageStringOf(x) 58 | } 59 | 60 | func (*GetRequest) ProtoMessage() {} 61 | 62 | func (x *GetRequest) ProtoReflect() protoreflect.Message { 63 | mi := &file_transport_pb_groupcache_proto_msgTypes[0] 64 | if protoimpl.UnsafeEnabled && x != nil { 65 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 66 | if ms.LoadMessageInfo() == nil { 67 | ms.StoreMessageInfo(mi) 68 | } 69 | return ms 70 | } 71 | return mi.MessageOf(x) 72 | } 73 | 74 | // Deprecated: Use GetRequest.ProtoReflect.Descriptor instead. 75 | func (*GetRequest) Descriptor() ([]byte, []int) { 76 | return file_transport_pb_groupcache_proto_rawDescGZIP(), []int{0} 77 | } 78 | 79 | func (x *GetRequest) GetGroup() string { 80 | if x != nil && x.Group != nil { 81 | return *x.Group 82 | } 83 | return "" 84 | } 85 | 86 | func (x *GetRequest) GetKey() string { 87 | if x != nil && x.Key != nil { 88 | return *x.Key 89 | } 90 | return "" 91 | } 92 | 93 | type GetResponse struct { 94 | state protoimpl.MessageState 95 | sizeCache protoimpl.SizeCache 96 | unknownFields protoimpl.UnknownFields 97 | 98 | Value []byte `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` 99 | MinuteQps *float64 `protobuf:"fixed64,2,opt,name=minute_qps,json=minuteQps" json:"minute_qps,omitempty"` 100 | Expire *int64 `protobuf:"varint,3,opt,name=expire" json:"expire,omitempty"` 101 | } 102 | 103 | func (x *GetResponse) Reset() { 104 | *x = GetResponse{} 105 | if protoimpl.UnsafeEnabled { 106 | mi := &file_transport_pb_groupcache_proto_msgTypes[1] 107 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 108 | ms.StoreMessageInfo(mi) 109 | } 110 | } 111 | 112 | func (x *GetResponse) String() string { 113 | return protoimpl.X.MessageStringOf(x) 114 | } 115 | 116 | func (*GetResponse) ProtoMessage() {} 117 | 118 | func (x *GetResponse) ProtoReflect() protoreflect.Message { 119 | mi := &file_transport_pb_groupcache_proto_msgTypes[1] 120 | if protoimpl.UnsafeEnabled && x != nil { 121 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 122 | if ms.LoadMessageInfo() == nil { 123 | ms.StoreMessageInfo(mi) 124 | } 125 | return ms 126 | } 127 | return mi.MessageOf(x) 128 | } 129 | 130 | // Deprecated: Use GetResponse.ProtoReflect.Descriptor instead. 131 | func (*GetResponse) Descriptor() ([]byte, []int) { 132 | return file_transport_pb_groupcache_proto_rawDescGZIP(), []int{1} 133 | } 134 | 135 | func (x *GetResponse) GetValue() []byte { 136 | if x != nil { 137 | return x.Value 138 | } 139 | return nil 140 | } 141 | 142 | func (x *GetResponse) GetMinuteQps() float64 { 143 | if x != nil && x.MinuteQps != nil { 144 | return *x.MinuteQps 145 | } 146 | return 0 147 | } 148 | 149 | func (x *GetResponse) GetExpire() int64 { 150 | if x != nil && x.Expire != nil { 151 | return *x.Expire 152 | } 153 | return 0 154 | } 155 | 156 | type SetRequest struct { 157 | state protoimpl.MessageState 158 | sizeCache protoimpl.SizeCache 159 | unknownFields protoimpl.UnknownFields 160 | 161 | Group *string `protobuf:"bytes,1,req,name=group" json:"group,omitempty"` 162 | Key *string `protobuf:"bytes,2,req,name=key" json:"key,omitempty"` 163 | Value []byte `protobuf:"bytes,3,opt,name=value" json:"value,omitempty"` 164 | Expire *int64 `protobuf:"varint,4,opt,name=expire" json:"expire,omitempty"` 165 | } 166 | 167 | func (x *SetRequest) Reset() { 168 | *x = SetRequest{} 169 | if protoimpl.UnsafeEnabled { 170 | mi := &file_transport_pb_groupcache_proto_msgTypes[2] 171 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 172 | ms.StoreMessageInfo(mi) 173 | } 174 | } 175 | 176 | func (x *SetRequest) String() string { 177 | return protoimpl.X.MessageStringOf(x) 178 | } 179 | 180 | func (*SetRequest) ProtoMessage() {} 181 | 182 | func (x *SetRequest) ProtoReflect() protoreflect.Message { 183 | mi := &file_transport_pb_groupcache_proto_msgTypes[2] 184 | if protoimpl.UnsafeEnabled && x != nil { 185 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 186 | if ms.LoadMessageInfo() == nil { 187 | ms.StoreMessageInfo(mi) 188 | } 189 | return ms 190 | } 191 | return mi.MessageOf(x) 192 | } 193 | 194 | // Deprecated: Use SetRequest.ProtoReflect.Descriptor instead. 195 | func (*SetRequest) Descriptor() ([]byte, []int) { 196 | return file_transport_pb_groupcache_proto_rawDescGZIP(), []int{2} 197 | } 198 | 199 | func (x *SetRequest) GetGroup() string { 200 | if x != nil && x.Group != nil { 201 | return *x.Group 202 | } 203 | return "" 204 | } 205 | 206 | func (x *SetRequest) GetKey() string { 207 | if x != nil && x.Key != nil { 208 | return *x.Key 209 | } 210 | return "" 211 | } 212 | 213 | func (x *SetRequest) GetValue() []byte { 214 | if x != nil { 215 | return x.Value 216 | } 217 | return nil 218 | } 219 | 220 | func (x *SetRequest) GetExpire() int64 { 221 | if x != nil && x.Expire != nil { 222 | return *x.Expire 223 | } 224 | return 0 225 | } 226 | 227 | var File_transport_pb_groupcache_proto protoreflect.FileDescriptor 228 | 229 | var file_transport_pb_groupcache_proto_rawDesc = []byte{ 230 | 0x0a, 0x1d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x70, 0x62, 0x2f, 0x67, 231 | 0x72, 0x6f, 0x75, 0x70, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 232 | 0x02, 0x70, 0x62, 0x22, 0x34, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 233 | 0x74, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x02, 0x28, 0x09, 234 | 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 235 | 0x20, 0x02, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x5a, 0x0a, 0x0b, 0x47, 0x65, 0x74, 236 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 237 | 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 238 | 0x0a, 0x0a, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x5f, 0x71, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, 239 | 0x28, 0x01, 0x52, 0x09, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x51, 0x70, 0x73, 0x12, 0x16, 0x0a, 240 | 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x65, 241 | 0x78, 0x70, 0x69, 0x72, 0x65, 0x22, 0x62, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 242 | 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x02, 243 | 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 244 | 0x18, 0x02, 0x20, 0x02, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 245 | 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 246 | 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 247 | 0x03, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x32, 0x36, 0x0a, 0x0a, 0x47, 0x72, 0x6f, 248 | 0x75, 0x70, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x28, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x0e, 249 | 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 250 | 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 251 | 0x00, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 252 | 0x67, 0x72, 0x6f, 0x75, 0x70, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70, 253 | 0x63, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x67, 0x6f, 0x2f, 0x70, 0x62, 254 | } 255 | 256 | var ( 257 | file_transport_pb_groupcache_proto_rawDescOnce sync.Once 258 | file_transport_pb_groupcache_proto_rawDescData = file_transport_pb_groupcache_proto_rawDesc 259 | ) 260 | 261 | func file_transport_pb_groupcache_proto_rawDescGZIP() []byte { 262 | file_transport_pb_groupcache_proto_rawDescOnce.Do(func() { 263 | file_transport_pb_groupcache_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_pb_groupcache_proto_rawDescData) 264 | }) 265 | return file_transport_pb_groupcache_proto_rawDescData 266 | } 267 | 268 | var file_transport_pb_groupcache_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 269 | var file_transport_pb_groupcache_proto_goTypes = []interface{}{ 270 | (*GetRequest)(nil), // 0: pb.GetRequest 271 | (*GetResponse)(nil), // 1: pb.GetResponse 272 | (*SetRequest)(nil), // 2: pb.SetRequest 273 | } 274 | var file_transport_pb_groupcache_proto_depIdxs = []int32{ 275 | 0, // 0: pb.GroupCache.Get:input_type -> pb.GetRequest 276 | 1, // 1: pb.GroupCache.Get:output_type -> pb.GetResponse 277 | 1, // [1:2] is the sub-list for method output_type 278 | 0, // [0:1] is the sub-list for method input_type 279 | 0, // [0:0] is the sub-list for extension type_name 280 | 0, // [0:0] is the sub-list for extension extendee 281 | 0, // [0:0] is the sub-list for field type_name 282 | } 283 | 284 | func init() { file_transport_pb_groupcache_proto_init() } 285 | func file_transport_pb_groupcache_proto_init() { 286 | if File_transport_pb_groupcache_proto != nil { 287 | return 288 | } 289 | if !protoimpl.UnsafeEnabled { 290 | file_transport_pb_groupcache_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 291 | switch v := v.(*GetRequest); i { 292 | case 0: 293 | return &v.state 294 | case 1: 295 | return &v.sizeCache 296 | case 2: 297 | return &v.unknownFields 298 | default: 299 | return nil 300 | } 301 | } 302 | file_transport_pb_groupcache_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 303 | switch v := v.(*GetResponse); i { 304 | case 0: 305 | return &v.state 306 | case 1: 307 | return &v.sizeCache 308 | case 2: 309 | return &v.unknownFields 310 | default: 311 | return nil 312 | } 313 | } 314 | file_transport_pb_groupcache_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 315 | switch v := v.(*SetRequest); i { 316 | case 0: 317 | return &v.state 318 | case 1: 319 | return &v.sizeCache 320 | case 2: 321 | return &v.unknownFields 322 | default: 323 | return nil 324 | } 325 | } 326 | } 327 | type x struct{} 328 | out := protoimpl.TypeBuilder{ 329 | File: protoimpl.DescBuilder{ 330 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 331 | RawDescriptor: file_transport_pb_groupcache_proto_rawDesc, 332 | NumEnums: 0, 333 | NumMessages: 3, 334 | NumExtensions: 0, 335 | NumServices: 1, 336 | }, 337 | GoTypes: file_transport_pb_groupcache_proto_goTypes, 338 | DependencyIndexes: file_transport_pb_groupcache_proto_depIdxs, 339 | MessageInfos: file_transport_pb_groupcache_proto_msgTypes, 340 | }.Build() 341 | File_transport_pb_groupcache_proto = out.File 342 | file_transport_pb_groupcache_proto_rawDesc = nil 343 | file_transport_pb_groupcache_proto_goTypes = nil 344 | file_transport_pb_groupcache_proto_depIdxs = nil 345 | } 346 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright Derrick J Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package groupcache 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "fmt" 24 | "sync" 25 | "time" 26 | 27 | "go.opentelemetry.io/otel/attribute" 28 | otelmetric "go.opentelemetry.io/otel/metric" 29 | 30 | "github.com/groupcache/groupcache-go/v3/internal/singleflight" 31 | "github.com/groupcache/groupcache-go/v3/transport" 32 | "github.com/groupcache/groupcache-go/v3/transport/pb" 33 | "github.com/groupcache/groupcache-go/v3/transport/peer" 34 | ) 35 | 36 | // Group is the user facing interface for a group 37 | type Group interface { 38 | // TODO: deprecate the hotCache boolean in Set(). It is not needed 39 | 40 | Set(context.Context, string, []byte, time.Time, bool) error 41 | Get(context.Context, string, transport.Sink) error 42 | Remove(context.Context, string) error 43 | UsedBytes() (int64, int64) 44 | Name() string 45 | } 46 | 47 | // A Getter loads data for a key. 48 | type Getter interface { 49 | // Get returns the value identified by key, populating dest. 50 | // 51 | // The returned data must be unversioned. That is, key must 52 | // uniquely describe the loaded data, without an implicit 53 | // current time, and without relying on cache expiration 54 | // mechanisms. 55 | Get(ctx context.Context, key string, dest transport.Sink) error 56 | } 57 | 58 | // A GetterFunc implements Getter with a function. 59 | type GetterFunc func(ctx context.Context, key string, dest transport.Sink) error 60 | 61 | func (f GetterFunc) Get(ctx context.Context, key string, dest transport.Sink) error { 62 | return f(ctx, key, dest) 63 | } 64 | 65 | // A Group is a cache namespace and associated data loaded spread over 66 | // a group of 1 or more machines. 67 | type group struct { 68 | name string 69 | getter Getter 70 | instance *Instance 71 | maxCacheBytes int64 // max size of both mainCache and hotCache 72 | 73 | // mainCache is a cache of the keys for which this process 74 | // (amongst its peers) is authoritative. That is, this cache 75 | // contains keys which consistent hash on to this process's 76 | // peer number. 77 | mainCache Cache 78 | 79 | // hotCache contains keys/values for which this peer is not 80 | // authoritative (otherwise they would be in mainCache), but 81 | // are popular enough to warrant mirroring in this process to 82 | // avoid going over the network to fetch from a peer. Having 83 | // a hotCache avoids network hot spotting, where a peer's 84 | // network card could become the bottleneck on a popular key. 85 | // This cache is used sparingly to maximize the total number 86 | // of key/value pairs that can be stored globally. 87 | hotCache Cache 88 | 89 | // loadGroup ensures that each key is only fetched once 90 | // (either locally or remotely), regardless of the number of 91 | // concurrent callers. 92 | loadGroup *singleflight.Group 93 | 94 | // setGroup ensures that each added key is only added 95 | // remotely once regardless of the number of concurrent callers. 96 | setGroup *singleflight.Group 97 | 98 | // removeGroup ensures that each removed key is only removed 99 | // remotely once regardless of the number of concurrent callers. 100 | removeGroup *singleflight.Group 101 | 102 | // Stats are statistics on the group. 103 | Stats GroupStats 104 | } 105 | 106 | // Name returns the name of the group. 107 | func (g *group) Name() string { 108 | return g.name 109 | } 110 | 111 | // UsedBytes returns the total number of bytes used by the main and hot caches 112 | func (g *group) UsedBytes() (mainCache int64, hotCache int64) { 113 | return g.mainCache.Bytes(), g.hotCache.Bytes() 114 | } 115 | 116 | func (g *group) Get(ctx context.Context, key string, dest transport.Sink) error { 117 | g.Stats.Gets.Add(1) 118 | if dest == nil { 119 | return errors.New("groupcache: nil dest Sink") 120 | } 121 | value, cacheHit := g.lookupCache(key) 122 | 123 | if cacheHit { 124 | g.Stats.CacheHits.Add(1) 125 | return transport.SetSinkView(dest, value) 126 | } 127 | 128 | // Optimization to avoid double unmarshalling or copying: keep 129 | // track of whether the dest was already populated. One caller 130 | // (if local) will set this; the losers will not. The common 131 | // case will likely be one caller. 132 | var destPopulated bool 133 | value, destPopulated, err := g.load(ctx, key, dest) 134 | if err != nil { 135 | return err 136 | } 137 | if destPopulated { 138 | return nil 139 | } 140 | return transport.SetSinkView(dest, value) 141 | } 142 | 143 | func (g *group) Set(ctx context.Context, key string, value []byte, expire time.Time, _ bool) error { 144 | if key == "" { 145 | return errors.New("empty Set() key not allowed") 146 | } 147 | 148 | if g.maxCacheBytes <= 0 { 149 | return nil 150 | } 151 | 152 | _, err := g.setGroup.Do(key, func() (interface{}, error) { 153 | // If remote peer owns this key 154 | owner, isRemote := g.instance.PickPeer(key) 155 | if isRemote { 156 | // Set the key/value on the remote peer 157 | if err := g.setPeer(ctx, owner, key, value, expire); err != nil { 158 | return nil, err 159 | } 160 | } 161 | // Update the local caches 162 | bv := transport.ByteViewWithExpire(value, expire) 163 | g.loadGroup.Lock(func() { 164 | g.mainCache.Add(key, bv) 165 | g.hotCache.Remove(key) 166 | }) 167 | 168 | // Update all peers in the cluster 169 | var wg sync.WaitGroup 170 | for _, p := range g.instance.getAllPeers() { 171 | if p.PeerInfo().IsSelf { 172 | continue // Skip self 173 | } 174 | 175 | // Do not update the owner again, we already updated them 176 | if p.HashKey() == owner.HashKey() { 177 | continue 178 | } 179 | 180 | wg.Add(1) 181 | go func(p peer.Client) { 182 | if err := g.setPeer(ctx, p, key, value, expire); err != nil { 183 | g.instance.opts.Logger.Error("while calling Set on peer", 184 | "peer", p.PeerInfo().Address, 185 | "key", key, 186 | "err", err) 187 | } 188 | wg.Done() 189 | }(p) 190 | } 191 | wg.Wait() 192 | 193 | return nil, nil 194 | }) 195 | return err 196 | } 197 | 198 | // Remove clears the key from our cache then forwards the remove 199 | // request to all peers. 200 | // 201 | // ### Consistency Warning 202 | // This method implements a best case design since it is possible a temporary network disruption could 203 | // occur resulting in remove requests never making it their peers. In practice this scenario is rare 204 | // and the system typically remains consistent. However, in case of an inconsistency we recommend placing 205 | // an expiration time on your values to ensure the cluster eventually becomes consistent again. 206 | func (g *group) Remove(ctx context.Context, key string) error { 207 | _, err := g.removeGroup.Do(key, func() (interface{}, error) { 208 | 209 | // Remove from key owner first 210 | owner, isRemote := g.instance.PickPeer(key) 211 | if isRemote { 212 | if err := g.removeFromPeer(ctx, owner, key); err != nil { 213 | return nil, err 214 | } 215 | } 216 | // Remove from our cache next 217 | g.LocalRemove(key) 218 | wg := sync.WaitGroup{} 219 | errCh := make(chan error) 220 | 221 | // Asynchronously clear the key from all hot and main caches of peers 222 | for _, p := range g.instance.getAllPeers() { 223 | // avoid deleting from owner a second time 224 | if p == owner { 225 | continue 226 | } 227 | 228 | wg.Add(1) 229 | go func(p peer.Client) { 230 | errCh <- g.removeFromPeer(ctx, p, key) 231 | wg.Done() 232 | }(p) 233 | } 234 | go func() { 235 | wg.Wait() 236 | close(errCh) 237 | }() 238 | 239 | m := &MultiError{} 240 | for err := range errCh { 241 | m.Add(err) 242 | } 243 | 244 | return nil, m.NilOrError() 245 | }) 246 | return err 247 | } 248 | 249 | // load loads key either by invoking the getter locally or by sending it to another machine. 250 | func (g *group) load(ctx context.Context, key string, dest transport.Sink) (value transport.ByteView, destPopulated bool, err error) { 251 | g.Stats.Loads.Add(1) 252 | viewi, err := g.loadGroup.Do(key, func() (interface{}, error) { 253 | // Check the cache again because singleflight can only dedup calls 254 | // that overlap concurrently. It's possible for 2 concurrent 255 | // requests to miss the cache, resulting in 2 load() calls. An 256 | // unfortunate goroutine scheduling would result in this callback 257 | // being run twice, serially. If we don't check the cache again, 258 | // cache.nbytes would be incremented below even though there will 259 | // be only one entry for this key. 260 | // 261 | // Consider the following serialized event ordering for two 262 | // goroutines in which this callback gets called twice for hte 263 | // same key: 264 | // 1: Get("key") 265 | // 2: Get("key") 266 | // 1: lookupCache("key") 267 | // 2: lookupCache("key") 268 | // 1: load("key") 269 | // 2: load("key") 270 | // 1: loadGroup.Do("key", fn) 271 | // 1: fn() 272 | // 2: loadGroup.Do("key", fn) 273 | // 2: fn() 274 | if value, cacheHit := g.lookupCache(key); cacheHit { 275 | g.Stats.CacheHits.Add(1) 276 | return value, nil 277 | } 278 | g.Stats.LoadsDeduped.Add(1) 279 | var value transport.ByteView 280 | var err error 281 | if peer, ok := g.instance.PickPeer(key); ok { 282 | 283 | // metrics duration start 284 | start := time.Now() 285 | 286 | // get value from peers 287 | value, err = g.getFromPeer(ctx, peer, key) 288 | 289 | // metrics duration compute 290 | duration := int64(time.Since(start)) / int64(time.Millisecond) 291 | 292 | // metrics only store the slowest duration 293 | if g.Stats.GetFromPeersLatencyLower.Get() < duration { 294 | g.Stats.GetFromPeersLatencyLower.Store(duration) 295 | } 296 | 297 | if err == nil { 298 | g.Stats.PeerLoads.Add(1) 299 | return value, nil 300 | } 301 | 302 | if errors.Is(err, context.Canceled) { 303 | return nil, err 304 | } 305 | 306 | if errors.Is(err, &transport.ErrNotFound{}) { 307 | return nil, err 308 | } 309 | 310 | if errors.Is(err, &transport.ErrRemoteCall{}) { 311 | return nil, err 312 | } 313 | 314 | if g.instance.opts.Logger != nil { 315 | g.instance.opts.Logger.Error( 316 | "while retrieving key from peer", 317 | "peer", peer.PeerInfo().Address, 318 | "category", "groupcache", 319 | "err", err, 320 | "key", key) 321 | } 322 | 323 | g.Stats.PeerErrors.Add(1) 324 | if ctx != nil && ctx.Err() != nil { 325 | // Return here without attempting to get locally 326 | // since the context is no longer valid 327 | return nil, err 328 | } 329 | } 330 | 331 | value, err = g.getLocally(ctx, key, dest) 332 | if err != nil { 333 | g.Stats.LocalLoadErrs.Add(1) 334 | return nil, err 335 | } 336 | g.Stats.LocalLoads.Add(1) 337 | destPopulated = true // only one caller of load gets this return value 338 | g.populateCache(key, value, g.mainCache) 339 | return value, nil 340 | }) 341 | if err == nil { 342 | value = viewi.(transport.ByteView) 343 | } 344 | return 345 | } 346 | 347 | func (g *group) getLocally(ctx context.Context, key string, dest transport.Sink) (transport.ByteView, error) { 348 | err := g.getter.Get(ctx, key, dest) 349 | if err != nil { 350 | return transport.ByteView{}, err 351 | } 352 | return dest.View() 353 | } 354 | 355 | func (g *group) getFromPeer(ctx context.Context, peer peer.Client, key string) (transport.ByteView, error) { 356 | req := &pb.GetRequest{ 357 | Group: &g.name, 358 | Key: &key, 359 | } 360 | res := &pb.GetResponse{} 361 | err := peer.Get(ctx, req, res) 362 | if err != nil { 363 | return transport.ByteView{}, err 364 | } 365 | 366 | var expire time.Time 367 | if res.Expire != nil && *res.Expire != 0 { 368 | expire = time.Unix(*res.Expire/int64(time.Second), *res.Expire%int64(time.Second)) 369 | } 370 | 371 | value := transport.ByteViewWithExpire(res.Value, expire) 372 | 373 | // Always populate the hot cache 374 | g.populateCache(key, value, g.hotCache) 375 | return value, nil 376 | } 377 | 378 | func (g *group) setPeer(ctx context.Context, peer peer.Client, k string, v []byte, e time.Time) error { 379 | var expire int64 380 | if !e.IsZero() { 381 | expire = e.UnixNano() 382 | } 383 | req := &pb.SetRequest{ 384 | Expire: &expire, 385 | Group: &g.name, 386 | Key: &k, 387 | Value: v, 388 | } 389 | return peer.Set(ctx, req) 390 | } 391 | 392 | func (g *group) removeFromPeer(ctx context.Context, peer peer.Client, key string) error { 393 | req := &pb.GetRequest{ 394 | Group: &g.name, 395 | Key: &key, 396 | } 397 | return peer.Remove(ctx, req) 398 | } 399 | 400 | func (g *group) lookupCache(key string) (value transport.ByteView, ok bool) { 401 | if g.maxCacheBytes <= 0 { 402 | return 403 | } 404 | value, ok = g.mainCache.Get(key) 405 | if ok { 406 | return 407 | } 408 | value, ok = g.hotCache.Get(key) 409 | return 410 | } 411 | 412 | // RemoteSet is called by the transport to set values in the local and hot caches when 413 | // a remote peer sends us a pb.SetRequest 414 | func (g *group) RemoteSet(key string, value []byte, expire time.Time) { 415 | if g.maxCacheBytes <= 0 { 416 | return 417 | } 418 | 419 | // Lock all load operations until this function returns 420 | g.loadGroup.Lock(func() { 421 | // This instance could take over ownership of this key at any moment after 422 | // the set is made. In order to avoid accidental propagation of the previous 423 | // value should this instance become owner of the key, we always set key in 424 | // the main cache. 425 | bv := transport.ByteViewWithExpire(value, expire) 426 | g.mainCache.Add(key, bv) 427 | 428 | // It's possible the value could be in the hot cache. 429 | g.hotCache.Remove(key) 430 | }) 431 | } 432 | 433 | func (g *group) LocalRemove(key string) { 434 | // Clear key from our local cache 435 | if g.maxCacheBytes <= 0 { 436 | return 437 | } 438 | 439 | // Ensure no requests are in flight 440 | g.loadGroup.Lock(func() { 441 | g.hotCache.Remove(key) 442 | g.mainCache.Remove(key) 443 | }) 444 | } 445 | 446 | func (g *group) populateCache(key string, value transport.ByteView, cache Cache) { 447 | if g.maxCacheBytes <= 0 { 448 | return 449 | } 450 | cache.Add(key, value) 451 | } 452 | 453 | // CacheType represents a type of cache. 454 | type CacheType int 455 | 456 | const ( 457 | // The MainCache is the cache for items that this peer is the 458 | // owner for. 459 | MainCache CacheType = iota + 1 460 | 461 | // The HotCache is the cache for items that seem popular 462 | // enough to replicate to this node, even though it's not the 463 | // owner. 464 | HotCache 465 | ) 466 | 467 | // CacheStats returns stats about the provided cache within the group. 468 | func (g *group) CacheStats(which CacheType) CacheStats { 469 | switch which { 470 | case MainCache: 471 | return g.mainCache.Stats() 472 | case HotCache: 473 | return g.hotCache.Stats() 474 | default: 475 | return CacheStats{} 476 | } 477 | } 478 | 479 | // ResetCacheSize changes the maxBytes allowed and resets both the main and hot caches. 480 | // It is mostly intended for testing and is not thread safe. 481 | func (g *group) ResetCacheSize(maxBytes int64) error { 482 | g.maxCacheBytes = maxBytes 483 | var ( 484 | hotCache int64 485 | mainCache int64 486 | ) 487 | 488 | // Avoid divide by zero 489 | if maxBytes >= 0 { 490 | // Hot cache is 1/8th the size of the main cache 491 | hotCache = maxBytes / 8 492 | mainCache = hotCache * 7 493 | } 494 | 495 | var err error 496 | g.mainCache, err = g.instance.opts.CacheFactory(mainCache) 497 | if err != nil { 498 | return fmt.Errorf("Options.CacheFactory(): %w", err) 499 | } 500 | g.hotCache, err = g.instance.opts.CacheFactory(hotCache) 501 | if err != nil { 502 | return fmt.Errorf("Options.CacheFactory(): %w", err) 503 | } 504 | return nil 505 | } 506 | 507 | func (g *group) registerInstruments(meter otelmetric.Meter) error { 508 | instruments, err := newGroupInstruments(meter) 509 | if err != nil { 510 | return err 511 | } 512 | 513 | observeOptions := []otelmetric.ObserveOption{ 514 | otelmetric.WithAttributes(attribute.String("group.name", g.Name())), 515 | } 516 | 517 | _, err = meter.RegisterCallback(func(ctx context.Context, o otelmetric.Observer) error { 518 | o.ObserveInt64(instruments.GetsCounter(), g.Stats.Gets.Get(), observeOptions...) 519 | o.ObserveInt64(instruments.HitsCounter(), g.Stats.CacheHits.Get(), observeOptions...) 520 | o.ObserveInt64(instruments.PeerLoadsCounter(), g.Stats.PeerLoads.Get(), observeOptions...) 521 | o.ObserveInt64(instruments.PeerErrorsCounter(), g.Stats.PeerErrors.Get(), observeOptions...) 522 | o.ObserveInt64(instruments.LoadsCounter(), g.Stats.Loads.Get(), observeOptions...) 523 | o.ObserveInt64(instruments.LoadsDedupedCounter(), g.Stats.LoadsDeduped.Get(), observeOptions...) 524 | o.ObserveInt64(instruments.LocalLoadsCounter(), g.Stats.LocalLoads.Get(), observeOptions...) 525 | o.ObserveInt64(instruments.LocalLoadErrsCounter(), g.Stats.LocalLoadErrs.Get(), observeOptions...) 526 | o.ObserveInt64(instruments.GetFromPeersLatencyMaxGauge(), g.Stats.GetFromPeersLatencyLower.Get(), observeOptions...) 527 | 528 | return nil 529 | }, 530 | instruments.GetsCounter(), 531 | instruments.HitsCounter(), 532 | instruments.PeerLoadsCounter(), 533 | instruments.PeerErrorsCounter(), 534 | instruments.LoadsCounter(), 535 | instruments.LoadsDedupedCounter(), 536 | instruments.LocalLoadsCounter(), 537 | instruments.LocalLoadErrsCounter(), 538 | instruments.GetFromPeersLatencyMaxGauge(), 539 | ) 540 | 541 | return err 542 | } 543 | -------------------------------------------------------------------------------- /instance_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. 3 | Copyright 2024 Derrick J Wippler 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package groupcache_test 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "io" 24 | "log/slog" 25 | "math/rand" 26 | "net" 27 | "strings" 28 | "sync" 29 | "testing" 30 | "time" 31 | 32 | "github.com/stretchr/testify/assert" 33 | "github.com/stretchr/testify/require" 34 | "go.opentelemetry.io/otel/metric" 35 | "go.opentelemetry.io/otel/metric/noop" 36 | "google.golang.org/protobuf/proto" 37 | 38 | "github.com/groupcache/groupcache-go/v3" 39 | "github.com/groupcache/groupcache-go/v3/cluster" 40 | "github.com/groupcache/groupcache-go/v3/transport" 41 | "github.com/groupcache/groupcache-go/v3/transport/pb/testpb" 42 | ) 43 | 44 | type TestGroup interface { 45 | Get(ctx context.Context, key string, dest transport.Sink) error 46 | CacheStats(which groupcache.CacheType) groupcache.CacheStats 47 | ResetCacheSize(int64) error 48 | } 49 | 50 | var ( 51 | once sync.Once 52 | protoGroup transport.Group 53 | stringGroup TestGroup 54 | 55 | stringCh = make(chan string) 56 | 57 | dummyCtx context.Context 58 | 59 | // cacheFills is the number of times stringGroup or 60 | // protoGroup's Getter have been called. Read using the 61 | // cacheFills function. 62 | cacheFills groupcache.AtomicInt 63 | ) 64 | 65 | const ( 66 | stringGroupName = "string-group" 67 | protoGroupName = "proto-group" 68 | expireGroupName = "expire-group" 69 | fromChan = "from-chan" 70 | cacheSize = 1 << 20 71 | ) 72 | 73 | func testSetup() { 74 | instance := groupcache.New(groupcache.Options{}) 75 | 76 | g, _ := instance.NewGroup(stringGroupName, cacheSize, 77 | groupcache.GetterFunc(func(_ context.Context, key string, dest transport.Sink) error { 78 | if key == fromChan { 79 | key = <-stringCh 80 | } 81 | cacheFills.Add(1) 82 | return dest.SetString("ECHO:"+key, time.Time{}) 83 | })) 84 | stringGroup = g.(TestGroup) 85 | 86 | protoGroup, _ = instance.NewGroup(protoGroupName, cacheSize, 87 | groupcache.GetterFunc(func(_ context.Context, key string, dest transport.Sink) error { 88 | if key == fromChan { 89 | key = <-stringCh 90 | } 91 | cacheFills.Add(1) 92 | return dest.SetProto(&testpb.TestMessage{ 93 | Name: proto.String("ECHO:" + key), 94 | City: proto.String("SOME-CITY"), 95 | }, time.Time{}) 96 | })) 97 | 98 | } 99 | 100 | // tests that a Getter's Get method is only called once with two 101 | // outstanding callers. This is the string variant. 102 | func TestGetDupSuppressString(t *testing.T) { 103 | once.Do(testSetup) 104 | // Start two getters. The first should block (waiting reading 105 | // from stringCh) and the second should latch on to the first 106 | // one. 107 | resc := make(chan string, 2) 108 | for i := 0; i < 2; i++ { 109 | go func() { 110 | var s string 111 | if err := stringGroup.Get(dummyCtx, fromChan, transport.StringSink(&s)); err != nil { 112 | resc <- "ERROR:" + err.Error() 113 | return 114 | } 115 | resc <- s 116 | }() 117 | } 118 | 119 | // Wait a bit so both goroutines get merged together via 120 | // singleflight. 121 | // TODO(bradfitz): decide whether there are any non-offensive 122 | // debug/test hooks that could be added to singleflight to 123 | // make a sleep here unnecessary. 124 | time.Sleep(250 * time.Millisecond) 125 | 126 | // Unblock the first getter, which should unblock the second 127 | // as well. 128 | stringCh <- "foo" 129 | 130 | for i := 0; i < 2; i++ { 131 | select { 132 | case v := <-resc: 133 | if v != "ECHO:foo" { 134 | t.Errorf("got %q; want %q", v, "ECHO:foo") 135 | } 136 | case <-time.After(5 * time.Second): 137 | t.Errorf("timeout waiting on getter #%d of 2", i+1) 138 | } 139 | } 140 | } 141 | 142 | // tests that a Getter's Get method is only called once with two 143 | // outstanding callers. This is the proto variant. 144 | func TestGetDupSuppressProto(t *testing.T) { 145 | once.Do(testSetup) 146 | // Start two getters. The first should block (waiting reading 147 | // from stringCh) and the second should latch on to the first 148 | // one. 149 | resc := make(chan *testpb.TestMessage, 2) 150 | for i := 0; i < 2; i++ { 151 | go func() { 152 | tm := new(testpb.TestMessage) 153 | if err := protoGroup.Get(dummyCtx, fromChan, transport.ProtoSink(tm)); err != nil { 154 | tm.Name = proto.String("ERROR:" + err.Error()) 155 | } 156 | resc <- tm 157 | }() 158 | } 159 | 160 | // Wait a bit so both goroutines get merged together via 161 | // singleflight. 162 | // TODO(bradfitz): decide whether there are any non-offensive 163 | // debug/test hooks that could be added to singleflight to 164 | // make a sleep here unnecessary. 165 | time.Sleep(250 * time.Millisecond) 166 | 167 | // Unblock the first getter, which should unblock the second 168 | // as well. 169 | stringCh <- "Fluffy" 170 | want := &testpb.TestMessage{ 171 | Name: proto.String("ECHO:Fluffy"), 172 | City: proto.String("SOME-CITY"), 173 | } 174 | for i := 0; i < 2; i++ { 175 | select { 176 | case v := <-resc: 177 | if !proto.Equal(v, want) { 178 | t.Errorf(" Got: %v\nWant: %v", v.String(), want.String()) 179 | } 180 | case <-time.After(5 * time.Second): 181 | t.Errorf("timeout waiting on getter #%d of 2", i+1) 182 | } 183 | } 184 | } 185 | 186 | func countFills(f func()) int64 { 187 | fills0 := cacheFills.Get() 188 | f() 189 | return cacheFills.Get() - fills0 190 | } 191 | func TestCachingExpire(t *testing.T) { 192 | var fills int 193 | instance := groupcache.New(groupcache.Options{}) 194 | g, err := instance.NewGroup(expireGroupName, cacheSize, 195 | groupcache.GetterFunc(func(_ context.Context, key string, dest transport.Sink) error { 196 | fills++ 197 | return dest.SetString("ECHO:"+key, time.Now().Add(time.Millisecond*500)) 198 | })) 199 | require.NoError(t, err) 200 | 201 | for i := 0; i < 3; i++ { 202 | var s string 203 | if err := g.Get(context.Background(), "TestCachingExpire-key", transport.StringSink(&s)); err != nil { 204 | t.Fatal(err) 205 | } 206 | if i == 1 { 207 | time.Sleep(time.Millisecond * 900) 208 | } 209 | } 210 | if fills != 2 { 211 | t.Errorf("expected 2 cache fill; got %d", fills) 212 | } 213 | } 214 | 215 | func TestCaching(t *testing.T) { 216 | once.Do(testSetup) 217 | fills := countFills(func() { 218 | for i := 0; i < 10; i++ { 219 | var s string 220 | if err := stringGroup.Get(dummyCtx, "TestCaching-key", transport.StringSink(&s)); err != nil { 221 | t.Fatal(err) 222 | } 223 | } 224 | }) 225 | if fills != 1 { 226 | t.Errorf("expected 1 cache fill; got %d", fills) 227 | } 228 | } 229 | 230 | func TestCacheEviction(t *testing.T) { 231 | once.Do(testSetup) 232 | testKey := "TestCacheEviction-key" 233 | getTestKey := func() { 234 | var res string 235 | for i := 0; i < 10; i++ { 236 | if err := stringGroup.Get(dummyCtx, testKey, transport.StringSink(&res)); err != nil { 237 | t.Fatal(err) 238 | } 239 | } 240 | } 241 | fills := countFills(getTestKey) 242 | if fills != 1 { 243 | t.Fatalf("expected 1 cache fill; got %d", fills) 244 | } 245 | 246 | stats := stringGroup.CacheStats(groupcache.MainCache) 247 | evict0 := stats.Evictions 248 | 249 | // Trash the cache with other keys. 250 | var bytesFlooded int64 251 | // cacheSize/len(testKey) is approximate 252 | for bytesFlooded < cacheSize+1024 { 253 | var res string 254 | key := fmt.Sprintf("dummy-key-%d", bytesFlooded) 255 | err := stringGroup.Get(dummyCtx, key, transport.StringSink(&res)) 256 | require.NoError(t, err) 257 | bytesFlooded += int64(len(key) + len(res)) 258 | } 259 | evicts := stringGroup.CacheStats(groupcache.MainCache).Evictions - evict0 260 | if evicts <= 0 { 261 | t.Errorf("evicts = %v; want more than 0", evicts) 262 | } 263 | 264 | // Test that the key is gone. 265 | fills = countFills(getTestKey) 266 | if fills != 1 { 267 | t.Fatalf("expected 1 cache fill after cache trashing; got %d", fills) 268 | } 269 | } 270 | 271 | const groupName = "group-a" 272 | 273 | func TestPeers(t *testing.T) { 274 | mockTransport := transport.NewMockTransport() 275 | err := cluster.Start(context.Background(), 3, groupcache.Options{ 276 | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 277 | Transport: mockTransport, 278 | }) 279 | require.NoError(t, err) 280 | defer func() { _ = cluster.Shutdown(context.Background()) }() 281 | 282 | var localHits, totalHits int 283 | newGetter := func(idx int) groupcache.Getter { 284 | return groupcache.GetterFunc(func(ctx context.Context, key string, dest transport.Sink) error { 285 | totalHits++ 286 | // Only record local (non-remote hits) 287 | if idx == 0 { 288 | localHits++ 289 | } 290 | return dest.SetString("got:"+key, time.Time{}) 291 | }) 292 | } 293 | 294 | var groups []TestGroup 295 | // Create a group for each instance in the cluster 296 | for idx, d := range cluster.ListDaemons() { 297 | g, err := d.GetInstance().NewGroup(groupName, 1<<20, newGetter(idx)) 298 | require.NoError(t, err) 299 | groups = append(groups, g.(TestGroup)) 300 | } 301 | 302 | resetCacheSize := func(maxBytes int64) { 303 | for _, g := range groups { 304 | _ = g.ResetCacheSize(maxBytes) 305 | } 306 | } 307 | 308 | run := func(t *testing.T, name string, n int, wantSummary string) { 309 | t.Helper() 310 | group := cluster.DaemonAt(0).GetInstance().GetGroup(groupName) 311 | 312 | // Reset counters 313 | localHits, totalHits = 0, 0 314 | mockTransport.Reset() 315 | 316 | // Generate the same ip addresses each run 317 | random := rand.New(rand.NewSource(0)) 318 | 319 | for i := 0; i < n; i++ { 320 | // We use ip addresses for keys as it gives us a much better distribution across peers 321 | // than fmt.Sprintf("key-%d", i) 322 | r := random.Int31() 323 | key := net.IPv4(192, byte(r>>16), byte(r>>8), byte(r)).String() 324 | 325 | var got string 326 | err := group.Get(context.Background(), key, transport.StringSink(&got)) 327 | if err != nil { 328 | t.Errorf("%s: error on key %q: %v", name, key, err) 329 | continue 330 | } 331 | 332 | want := "got:" + key 333 | if got != want { 334 | t.Errorf("%s: for key %q, got %q; want %q", name, key, got, want) 335 | } 336 | } 337 | 338 | summary := func() string { 339 | return fmt.Sprintf("total = %d localhost:1111 = %d %s", totalHits, localHits, mockTransport.Report("Get")) 340 | } 341 | 342 | if got := summary(); got != wantSummary { 343 | t.Errorf("%s: got %q; want %q", name, got, wantSummary) 344 | } 345 | } 346 | 347 | run(t, "base", 200, "total = 200 localhost:1111 = 96 localhost:1112 = 38 localhost:1113 = 66") 348 | // Verify cache was hit. All localHits and peers are gone as the hotCache has the data we need 349 | run(t, "base_cached", 200, "total = 0 localhost:1111 = 0 ") 350 | 351 | // Force no cache hits 352 | resetCacheSize(0) 353 | 354 | // With one of the peers being down. 355 | _ = cluster.DaemonAt(1).Shutdown(context.Background()) 356 | run(t, "one_peer_down", 200, "total = 200 localhost:1111 = 134 localhost:1113 = 66") 357 | } 358 | 359 | func TestTruncatingByteSliceTarget(t *testing.T) { 360 | once.Do(testSetup) 361 | 362 | var buf [100]byte 363 | s := buf[:] 364 | if err := stringGroup.Get(dummyCtx, "short", transport.TruncatingByteSliceSink(&s)); err != nil { 365 | t.Fatal(err) 366 | } 367 | if want := "ECHO:short"; string(s) != want { 368 | t.Errorf("short key got %q; want %q", s, want) 369 | } 370 | 371 | s = buf[:6] 372 | if err := stringGroup.Get(dummyCtx, "truncated", transport.TruncatingByteSliceSink(&s)); err != nil { 373 | t.Fatal(err) 374 | } 375 | if want := "ECHO:t"; string(s) != want { 376 | t.Errorf("truncated key got %q; want %q", s, want) 377 | } 378 | } 379 | 380 | func TestAllocatingByteSliceTarget(t *testing.T) { 381 | var dst []byte 382 | sink := transport.AllocatingByteSliceSink(&dst) 383 | 384 | inBytes := []byte("some bytes") 385 | err := sink.SetBytes(inBytes, time.Time{}) 386 | require.NoError(t, err) 387 | 388 | if want := "some bytes"; string(dst) != want { 389 | t.Errorf("SetBytes resulted in %q; want %q", dst, want) 390 | } 391 | 392 | v, err := sink.View() 393 | if err != nil { 394 | t.Fatalf("view after SetBytes failed: %v", err) 395 | } 396 | 397 | // Modify the original "some bytes" slice and the destination 398 | dst[0] = 'A' 399 | inBytes[0] = 'B' 400 | 401 | if v.String() != "some bytes" { 402 | t.Error("inBytes or dst share memory with the view") 403 | } 404 | 405 | if &inBytes[0] == &dst[0] { 406 | t.Error("inBytes and dst share memory") 407 | } 408 | } 409 | 410 | func TestNoDeDup(t *testing.T) { 411 | var totalHits int 412 | 413 | gc := groupcache.New(groupcache.Options{}) 414 | getter := groupcache.GetterFunc(func(ctx context.Context, key string, dest transport.Sink) error { 415 | time.Sleep(time.Second) 416 | totalHits++ 417 | return dest.SetString("value", time.Time{}) 418 | }) 419 | 420 | g, err := gc.NewGroup(groupName, 1<<20, getter) 421 | require.NoError(t, err) 422 | 423 | resultCh := make(chan string, 100) 424 | go func() { 425 | var wg sync.WaitGroup 426 | 427 | for i := 0; i < 100_000; i++ { 428 | wg.Add(1) 429 | go func() { 430 | defer wg.Done() 431 | var s string 432 | if err := g.Get(dummyCtx, "key", transport.StringSink(&s)); err != nil { 433 | resultCh <- "ERROR:" + err.Error() 434 | return 435 | } 436 | resultCh <- s 437 | }() 438 | } 439 | wg.Wait() 440 | close(resultCh) 441 | }() 442 | 443 | for v := range resultCh { 444 | if strings.HasPrefix(v, "ERROR:") { 445 | t.Errorf("Get() returned unexpected error '%s'", v) 446 | } 447 | } 448 | 449 | // If the singleflight callback doesn't double-check the cache again 450 | // upon entry, we would increment nbytes twice but the entry would 451 | // only be in the cache once. 452 | const wantBytes = int64(len("key") + len("value")) 453 | used, _ := g.UsedBytes() 454 | if used != wantBytes { 455 | t.Errorf("cache has %d bytes, want %d", used, wantBytes) 456 | } 457 | } 458 | 459 | func TestSetValueOnAllPeers(t *testing.T) { 460 | ctx := context.Background() 461 | err := cluster.Start(ctx, 3, groupcache.Options{ 462 | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 463 | }) 464 | require.NoError(t, err) 465 | defer func() { _ = cluster.Shutdown(context.Background()) }() 466 | 467 | // Create a group for each instance in the cluster 468 | var groups []groupcache.Group 469 | for _, d := range cluster.ListDaemons() { 470 | g, err := d.GetInstance().NewGroup("group", 1<<20, groupcache.GetterFunc(func(ctx context.Context, key string, dest transport.Sink) error { 471 | return dest.SetString("original-value", time.Time{}) 472 | })) 473 | require.NoError(t, err) 474 | groups = append(groups, g) 475 | } 476 | 477 | // Set the value on the first group 478 | err = groups[0].Set(ctx, "key", []byte("value"), time.Time{}, false) 479 | require.NoError(t, err) 480 | 481 | // Verify the value exists on all peers 482 | for i, g := range groups { 483 | var result string 484 | err := g.Get(ctx, "key", transport.StringSink(&result)) 485 | require.NoError(t, err, "Failed to get value from peer %d", i) 486 | assert.Equal(t, "value", result, "Unexpected value from peer %d", i) 487 | } 488 | 489 | // Update the value on the second group 490 | err = groups[1].Set(ctx, "key", []byte("foo"), time.Time{}, false) 491 | require.NoError(t, err) 492 | 493 | // Verify the value was updated 494 | for i, g := range groups { 495 | var result string 496 | err := g.Get(ctx, "key", transport.StringSink(&result)) 497 | require.NoError(t, err, "Failed to get value from peer %d", i) 498 | assert.Equal(t, "foo", result, "Unexpected value from peer %d", i) 499 | } 500 | } 501 | 502 | func TestNewGroupRegistersMetricsWithMeterProvider(t *testing.T) { 503 | recMeter := &recordingMeter{} 504 | recProvider := &recordingMeterProvider{meter: recMeter} 505 | mp := groupcache.NewMeterProvider(groupcache.WithMeterProvider(recProvider)) 506 | 507 | instance := groupcache.New(groupcache.Options{ 508 | MetricProvider: mp, 509 | }) 510 | 511 | g, err := instance.NewGroup("metrics-group", cacheSize, groupcache.GetterFunc(func(_ context.Context, key string, dest transport.Sink) error { 512 | return dest.SetString("ok", time.Time{}) 513 | })) 514 | require.NoError(t, err) 515 | require.NotNil(t, g) 516 | 517 | expectedCounters := []string{ 518 | "groupcache.group.gets", 519 | "groupcache.group.cache_hits", 520 | "groupcache.group.peer.loads", 521 | "groupcache.group.peer.errors", 522 | "groupcache.group.loads", 523 | "groupcache.group.loads.deduped", 524 | "groupcache.group.local.loads", 525 | "groupcache.group.local.load_errors", 526 | } 527 | assert.Equal(t, expectedCounters, recMeter.counterNames) 528 | assert.Equal(t, []string{"groupcache.group.peer.latency_max_ms"}, recMeter.updownNames) 529 | assert.True(t, recMeter.callbackRegistered, "expected callback registration for metrics") 530 | assert.Equal(t, 9, recMeter.instrumentCount) 531 | } 532 | 533 | func TestNewGroupFailsWhenMetricRegistrationFails(t *testing.T) { 534 | failMeter := &failingMeter{} 535 | recProvider := &recordingMeterProvider{meter: failMeter} 536 | mp := groupcache.NewMeterProvider(groupcache.WithMeterProvider(recProvider)) 537 | 538 | instance := groupcache.New(groupcache.Options{ 539 | MetricProvider: mp, 540 | }) 541 | 542 | g, err := instance.NewGroup("metrics-group-error", cacheSize, groupcache.GetterFunc(func(_ context.Context, key string, dest transport.Sink) error { 543 | return dest.SetString("ok", time.Time{}) 544 | })) 545 | require.Error(t, err) 546 | assert.Nil(t, g) 547 | assert.True(t, failMeter.counterCalled, "expected metrics creation to be attempted") 548 | } 549 | 550 | type recordingMeterProvider struct { 551 | noop.MeterProvider 552 | meter metric.Meter 553 | } 554 | 555 | func (p *recordingMeterProvider) Meter(string, ...metric.MeterOption) metric.Meter { 556 | return p.meter 557 | } 558 | 559 | type recordingMeter struct { 560 | noop.Meter 561 | 562 | counterNames []string 563 | updownNames []string 564 | callbackRegistered bool 565 | instrumentCount int 566 | } 567 | 568 | func (m *recordingMeter) Int64ObservableCounter(name string, _ ...metric.Int64ObservableCounterOption) (metric.Int64ObservableCounter, error) { 569 | m.counterNames = append(m.counterNames, name) 570 | return noop.Int64ObservableCounter{}, nil 571 | } 572 | 573 | func (m *recordingMeter) Int64ObservableUpDownCounter(name string, _ ...metric.Int64ObservableUpDownCounterOption) (metric.Int64ObservableUpDownCounter, error) { 574 | m.updownNames = append(m.updownNames, name) 575 | return noop.Int64ObservableUpDownCounter{}, nil 576 | } 577 | 578 | func (m *recordingMeter) RegisterCallback(f metric.Callback, instruments ...metric.Observable) (metric.Registration, error) { 579 | m.callbackRegistered = true 580 | m.instrumentCount = len(instruments) 581 | // Invoke the callback once to ensure it tolerates being called with nil ctx/observer 582 | _ = f(context.Background(), noop.Observer{}) 583 | return noop.Registration{}, nil 584 | } 585 | 586 | type failingMeter struct { 587 | noop.Meter 588 | counterCalled bool 589 | } 590 | 591 | func (m *failingMeter) Int64ObservableCounter(string, ...metric.Int64ObservableCounterOption) (metric.Int64ObservableCounter, error) { 592 | m.counterCalled = true 593 | return nil, fmt.Errorf("cannot create counter") 594 | } 595 | --------------------------------------------------------------------------------