diff --git a/go.mod b/go.mod
index 3bc6ace4..810c49d6 100644
--- a/go.mod
+++ b/go.mod
@@ -9,9 +9,11 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
+ github.com/mailgun/groupcache/v2 v2.5.0
github.com/maypok86/otter v1.2.1
github.com/mitchellh/mapstructure v1.5.0
github.com/redis/go-redis/v9 v9.5.3
+ github.com/smartystreets/goconvey v1.8.1
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
@@ -26,15 +28,21 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gammazero/deque v0.2.1 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/segmentio/fasthash v1.0.3 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/smarty/assertions v1.15.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
@@ -48,5 +56,6 @@ require (
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
diff --git a/go.sum b/go.sum
index 402e448a..3e13b1c7 100644
--- a/go.sum
+++ b/go.sum
@@ -47,6 +47,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -58,6 +60,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -70,6 +74,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
@@ -83,6 +89,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mailgun/groupcache/v2 v2.5.0 h1:FoNR52GyTQ4jLoliSuyXDANMEoxts6M8ql9jW3htvq8=
+github.com/mailgun/groupcache/v2 v2.5.0/go.mod h1:7+O6vXEKAhloSTOJOmkhyksS8l/gIs15fv0ER1ZuhPA=
github.com/maypok86/otter v1.2.1 h1:xyvMW+t0vE1sKt/++GTkznLitEl7D/msqXkAbLwiC1M=
github.com/maypok86/otter v1.2.1/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -129,6 +137,14 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
+github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
+github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
+github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
+github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@@ -222,6 +238,7 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -264,6 +281,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
diff --git a/internal/caches/group.go b/internal/caches/group.go
new file mode 100644
index 00000000..072a76ab
--- /dev/null
+++ b/internal/caches/group.go
@@ -0,0 +1,59 @@
+package caches
+
+import (
+ "fmt"
+
+ "github.com/Michad/tilegroxy/internal"
+ "github.com/Michad/tilegroxy/internal/caches/group"
+ "github.com/Michad/tilegroxy/internal/config"
+)
+
+// GroupConfig is a local type so callers needn't import caches/group directly.
+type GroupConfig group.Config
+
+// GroupCache is a caches.Cache to use a groupcache as a cache.
+type GroupCache struct {
+ *group.Cache
+ conf group.Config
+}
+
+// Lookup takes a TileRequest and returns an Image or an error.
+func (g *GroupCache) Lookup(t internal.TileRequest) (*internal.Image, error) {
+ file := fmt.Sprintf("%d/%d/%d", t.Z, t.X, t.Y) // TODO
+ if v, ok := g.Cache.Get(t.LayerName, file); ok {
+ i := v.(internal.Image)
+ return &i, nil
+ }
+ return nil, group.ItemNotFoundError
+}
+
+// Save takes a TileRequest and an Image, and returns an error if it cannot be set.
+func (g *GroupCache) Save(t internal.TileRequest, img *internal.Image) error {
+ file := fmt.Sprintf("%d/%d/%d", t.Z, t.X, t.Y) // TODO
+ if !g.Exists(t.LayerName) {
+ // Add the cache if it doesn't exist
+ conf := g.conf
+ conf.Name = t.LayerName
+ g.Add(conf, nil)
+ }
+ return g.Cache.Set(t.LayerName, file, *img)
+}
+
+// NewGroupCache creates a new GroupCache from the specified config and returns it, or returns an error.
+func ConstructGroupCache(conf GroupConfig, errorMessages *config.ErrorMessages) (*GroupCache, error) {
+
+ // GroupCache has the concept of a backfill, whereby it will pull from another source on cache miss,
+ // helping reduce hotspots and distributing the pull load.
+ //
+ // This feature isn't compatible with the []Cache concept, as is, so we set the backfill to nil, and
+ // instead will manually "Save()"
+ gconf := group.Config(conf)
+ gc, err := group.NewCache(gconf, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &GroupCache{
+ Cache: gc,
+ conf: gconf,
+ }, nil
+}
diff --git a/internal/caches/group/Readme.md b/internal/caches/group/Readme.md
new file mode 100644
index 00000000..68a05c45
--- /dev/null
+++ b/internal/caches/group/Readme.md
@@ -0,0 +1,282 @@
+
+
+# group
+`import "github.com/Michad/tilegroxy/internal/caches/group"`
+
+* [Overview](#pkg-overview)
+* [Index](#pkg-index)
+
+## Overview
+
+
+
+## Index
+* [Constants](#pkg-constants)
+* [type BackFillFunc](#BackFillFunc)
+* [type Cache](#Cache)
+ * [func NewCache(config Config, fillfunc BackFillFunc) (*Cache, error)](#NewCache)
+ * [func (gc *Cache) Add(config Config, fillfunc BackFillFunc) error](#Cache.Add)
+ * [func (gc *Cache) Close() error](#Cache.Close)
+ * [func (gc *Cache) Exists(name string) bool](#Cache.Exists)
+ * [func (gc *Cache) Get(cacheName, key string) (value interface{}, ok bool)](#Cache.Get)
+ * [func (gc *Cache) GetContext(ctx context.Context, cacheName, key string) (value interface{}, ok bool)](#Cache.GetContext)
+ * [func (gc *Cache) Names() []string](#Cache.Names)
+ * [func (gc *Cache) Remove(cacheName, key string) error](#Cache.Remove)
+ * [func (gc *Cache) RemoveContext(ctx context.Context, cacheName, key string) error](#Cache.RemoveContext)
+ * [func (gc *Cache) Set(cacheName, key string, value []byte) error](#Cache.Set)
+ * [func (gc *Cache) SetContext(ctx context.Context, cacheName, key string, value []byte, expiration time.Time) error](#Cache.SetContext)
+ * [func (gc *Cache) SetDebugOut(logger *log.Logger)](#Cache.SetDebugOut)
+ * [func (gc *Cache) SetPeers(peers ...string)](#Cache.SetPeers)
+ * [func (gc *Cache) SetToExpireAt(cacheName, key string, expireAt time.Time, value []byte) error](#Cache.SetToExpireAt)
+ * [func (gc *Cache) Stats(w http.ResponseWriter, req *http.Request)](#Cache.Stats)
+* [type Config](#Config)
+* [type Error](#Error)
+ * [func (e Error) Error() string](#Error.Error)
+
+
+#### Package files
+[group.go](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go) [misc.go](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/misc.go)
+
+
+## Constants
+``` go
+const (
+ // NilBackfillError is returned by the Getter if there there is no backfill func, in lieu of panicing
+ NilBackfillError = Error("item not in cache and backfill func is nil")
+ // ItemNotFoundError is a generic error returned by a BackFillFunc if the item is not found or findable
+ ItemNotFoundError = Error("item not found")
+ // CacheNotFoundError is an error returned if the cache requested is not found
+ CacheNotFoundError = Error("cache not found")
+ // NameRequiredError is returned when creating or adding a cache, and the Config.Name field is empty
+ NameRequiredError = Error("name is required")
+)
+```
+
+
+
+
+## type [BackFillFunc](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=831:881#L27)
+``` go
+type BackFillFunc func(key string) ([]byte, error)
+```
+BackFillFunc is a function that can retrieve an uncached item to go into the cache
+
+
+
+
+
+
+
+
+
+
+## type [Cache](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=1090:1293#L31)
+``` go
+type Cache struct {
+ // contains filtered or unexported fields
+}
+
+```
+Cache (group) is a distributed LRU cache where consistent hashing on keynames is used to cut out
+"who's on first" nonsense, and backfills are linearly distributed to mitigate multiple-member requests.
+
+
+
+
+
+
+
+### func [NewCache](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=1479:1546#L44)
+``` go
+func NewCache(config Config, fillfunc BackFillFunc) (*Cache, error)
+```
+NewCache creates a ache from the Config. Only call this once. If you need
+more caches use the .Add() function. fillfunc may be nil if caches will be added later
+using .Add().
+
+
+
+
+
+### func (\*Cache) [Add](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=2316:2380#L81)
+``` go
+func (gc *Cache) Add(config Config, fillfunc BackFillFunc) error
+```
+Add creates new caches in the cluster. Config.ListenAddress and Config.PeerList are ignored.
+
+
+
+
+### func (\*Cache) [Close](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=3450:3480#L135)
+``` go
+func (gc *Cache) Close() error
+```
+Close calls the listener close function
+
+
+
+
+### func (\*Cache) [Exists](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=3247:3288#L124)
+``` go
+func (gc *Cache) Exists(name string) bool
+```
+Exists returns true if the named cache exists.
+
+
+
+
+### func (\*Cache) [Get](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=3617:3689#L141)
+``` go
+func (gc *Cache) Get(cacheName, key string) (value interface{}, ok bool)
+```
+Get will return the value of the cacheName'd key, asking other cache members or
+backfilling as necessary.
+
+
+
+
+### func (\*Cache) [GetContext](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=3905:4005#L147)
+``` go
+func (gc *Cache) GetContext(ctx context.Context, cacheName, key string) (value interface{}, ok bool)
+```
+GetContext will return the value of the cacheName'd key, asking other cache members or
+backfilling as necessary, honoring the provided context.
+
+
+
+
+### func (\*Cache) [Names](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=2999:3032#L110)
+``` go
+func (gc *Cache) Names() []string
+```
+Names returns the names of the current caches
+
+
+
+
+### func (\*Cache) [Remove](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=6289:6341#L198)
+``` go
+func (gc *Cache) Remove(cacheName, key string) error
+```
+Remove makes a best effort to remove an item from the cache
+
+
+
+
+### func (\*Cache) [RemoveContext](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=6512:6592#L203)
+``` go
+func (gc *Cache) RemoveContext(ctx context.Context, cacheName, key string) error
+```
+RemoveContext makes a best effort to remove an item from the cache, honoring the provided context.
+
+
+
+
+### func (\*Cache) [Set](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=4518:4581#L166)
+``` go
+func (gc *Cache) Set(cacheName, key string, value []byte) error
+```
+Set forces an item into the cache, following the configured expiration policy
+
+
+
+
+### func (\*Cache) [SetContext](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=4868:4981#L172)
+``` go
+func (gc *Cache) SetContext(ctx context.Context, cacheName, key string, value []byte, expiration time.Time) error
+```
+SetContext forces an item into the cache, following the specified expiration (unless a zero Time is provided
+then falling back to the configured expiration policy) honoring the provided context.
+
+
+
+
+### func (\*Cache) [SetDebugOut](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=6826:6874#L212)
+``` go
+func (gc *Cache) SetDebugOut(logger *log.Logger)
+```
+SetDebugOut wires in the debug logger to the specified logger
+
+
+
+
+### func (\*Cache) [SetPeers](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=6961:7003#L217)
+``` go
+func (gc *Cache) SetPeers(peers ...string)
+```
+SetPeers allows the dynamic [re]setting of the peerlist
+
+
+
+
+### func (\*Cache) [SetToExpireAt](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=5978:6071#L192)
+``` go
+func (gc *Cache) SetToExpireAt(cacheName, key string, expireAt time.Time, value []byte) error
+```
+SetToExpireAt forces an item into the cache, to expire at a specific time regardless of the cache configuration. Use
+SetContext if you need to set the expiration and a context.
+
+
+
+
+### func (\*Cache) [Stats](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/group.go?s=7100:7164#L222)
+``` go
+func (gc *Cache) Stats(w http.ResponseWriter, req *http.Request)
+```
+Stats is a request finisher that outputs the Cache stats as JSON
+
+
+
+
+## type [Config](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/misc.go?s=261:771#L16)
+``` go
+type Config struct {
+ Name string // For New and Add. Pass as ``cacheName`` to differentiate caches
+ ListenAddress string // Only for New to set the listener
+ PeerList []string // Only for New to establish the initial PeerList. May be reset with GroupCache.SetPeers()
+ CacheSize int64 // For New and Add to set the size in bytes of the cache
+ ItemExpiration time.Duration // For New and Add to set the default expiration duration. Leave as empty for infinite.
+}
+
+```
+Config is used to store configuration information to pass to a GroupCache.
+
+
+
+
+
+
+
+
+
+
+## type [Error](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/misc.go?s=61:78#L8)
+``` go
+type Error string
+```
+Error is an error type
+
+
+
+
+
+
+
+
+
+
+### func (Error) [Error](https://github.com/Michad/tilegroxy/tree/master/internal/caches/group/misc.go?s=130:159#L11)
+``` go
+func (e Error) Error() string
+```
+Error returns the stringified version of Error
+
+
+
+
+
+
+
+
+- - -
+Generated by [godoc2md](http://github.com/cognusion/godoc2md)
diff --git a/internal/caches/group/group.go b/internal/caches/group/group.go
new file mode 100644
index 00000000..76dac153
--- /dev/null
+++ b/internal/caches/group/group.go
@@ -0,0 +1,268 @@
+package group
+
+import (
+ "github.com/mailgun/groupcache/v2"
+
+ "context"
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "sync"
+ "time"
+)
+
+const (
+ // NilBackfillError is returned by the Getter if there there is no backfill func, in lieu of panicing
+ NilBackfillError = Error("item not in cache and backfill func is nil")
+ // ItemNotFoundError is a generic error returned by a BackFillFunc if the item is not found or findable
+ ItemNotFoundError = Error("item not found")
+ // CacheNotFoundError is an error returned if the cache requested is not found
+ CacheNotFoundError = Error("cache not found")
+ // NameRequiredError is returned when creating or adding a cache, and the Config.Name field is empty
+ NameRequiredError = Error("name is required")
+)
+
+// BackFillFunc is a function that can retrieve an uncached item to go into the cache
+type BackFillFunc func(key string) ([]byte, error)
+
+// Cache (group) is a distributed LRU cache where consistent hashing on keynames is used to cut out
+// "who's on first" nonsense, and backfills are linearly distributed to mitigate multiple-member requests.
+type Cache struct {
+ addr string
+ caches map[string]*groupcache.Group
+ pool *groupcache.HTTPPool
+ configs map[string]*Config
+ close func() error
+ debugOut *log.Logger
+ regLock sync.Mutex
+}
+
+// NewCache creates a ache from the Config. Only call this once. If you need
+// more caches use the .Add() function. fillfunc may be nil if caches will be added later
+// using .Add().
+func NewCache(config Config, fillfunc BackFillFunc) (*Cache, error) {
+
+ srv := http.Server{}
+ mux := http.NewServeMux()
+
+ pool := groupcache.NewHTTPPoolOpts(config.PeerList[0], nil)
+ pool.Set(config.PeerList...)
+ mux.Handle("/", pool)
+
+ srv.Handler = mux
+ srv.Addr = config.ListenAddress
+
+ gc := Cache{
+ addr: config.ListenAddress,
+ debugOut: log.New(io.Discard, "[DEBUG] ", 0),
+ pool: pool,
+ configs: make(map[string]*Config),
+ caches: make(map[string]*groupcache.Group),
+ close: srv.Close,
+ }
+
+ if fillfunc != nil {
+ if err := gc.Add(config, fillfunc); err != nil {
+ return nil, err
+ }
+ }
+
+ mux.HandleFunc("/stats", gc.Stats)
+
+ go func(server *http.Server) {
+ server.ListenAndServe()
+ }(&srv)
+
+ return &gc, nil
+}
+
+// Add creates new caches in the cluster. Config.ListenAddress and Config.PeerList are ignored.
+func (gc *Cache) Add(config Config, fillfunc BackFillFunc) error {
+
+ var gf groupcache.GetterFunc = func(ctx context.Context, key string, dest groupcache.Sink) error {
+
+ if fillfunc == nil {
+ return NilBackfillError
+ }
+
+ value, err := fillfunc(key)
+ if err != nil {
+ return err
+ }
+ if config.ItemExpiration == 0 {
+ dest.SetBytes(value, time.Time{})
+ } else {
+ dest.SetBytes(value, time.Now().Add(config.ItemExpiration))
+ }
+ return nil
+ }
+
+ gc.regLock.Lock()
+ defer gc.regLock.Unlock()
+
+ gc.caches[config.Name] = groupcache.NewGroup(config.Name, config.CacheSize, gf)
+ gc.configs[config.Name] = &config
+ return nil
+}
+
+// Names returns the names of the current caches
+func (gc *Cache) Names() []string {
+ gc.regLock.Lock()
+ defer gc.regLock.Unlock()
+
+ list := make([]string, len(gc.caches))
+ i := 0
+ for k := range gc.caches {
+ list[i] = k
+ i++
+ }
+ return list
+}
+
+// Exists returns true if the named cache exists.
+func (gc *Cache) Exists(name string) bool {
+ gc.regLock.Lock()
+ defer gc.regLock.Unlock()
+
+ if _, ok := gc.caches[name]; ok {
+ return true
+ }
+ return false
+}
+
+// Close calls the listener close function
+func (gc *Cache) Close() error {
+ return gc.close()
+}
+
+// Get will return the value of the cacheName'd key, asking other cache members or
+// backfilling as necessary.
+func (gc *Cache) Get(cacheName, key string) (value interface{}, ok bool) {
+ return gc.GetContext(context.Background(), cacheName, key)
+}
+
+// GetContext will return the value of the cacheName'd key, asking other cache members or
+// backfilling as necessary, honoring the provided context.
+func (gc *Cache) GetContext(ctx context.Context, cacheName, key string) (value interface{}, ok bool) {
+ gc.debugOut.Printf("Getting %s %s\n", cacheName, key)
+ return gc.get(ctx, cacheName, key)
+}
+
+func (gc *Cache) get(ctx context.Context, cacheName, key string) (value interface{}, ok bool) {
+ if cache, ok := gc.caches[cacheName]; ok {
+ var b []byte
+ err := cache.Get(ctx, key, groupcache.AllocatingByteSliceSink(&b))
+ if err != nil {
+ // crap
+ return err, false
+ }
+ return b, true
+ }
+ return CacheNotFoundError, false
+}
+
+// Set forces an item into the cache, following the configured expiration policy
+func (gc *Cache) Set(cacheName, key string, value []byte) error {
+ return gc.SetContext(context.Background(), cacheName, key, value, time.Time{})
+}
+
+// SetContext forces an item into the cache, following the specified expiration (unless a zero Time is provided
+// then falling back to the configured expiration policy) honoring the provided context.
+func (gc *Cache) SetContext(ctx context.Context, cacheName, key string, value []byte, expiration time.Time) error {
+ gc.debugOut.Printf("Setting %s %s @ %s\n", cacheName, key, expiration.String())
+ return gc.set(ctx, cacheName, key, value, expiration)
+}
+
+// set is an internal function for all of the Set* funcs. expirationOption is either “true“ (follow policy),
+// “false“ (no expiration), or a time.Time specifying when to expire the item
+func (gc *Cache) set(ctx context.Context, cacheName, key string, value []byte, expiration time.Time) error {
+ if cache, ok := gc.caches[cacheName]; ok {
+ if expiration.IsZero() && gc.configs[cacheName].ItemExpiration != 0 {
+ // Local expiration is zero, but the cache has an expiration
+ return cache.Set(ctx, key, value, time.Now().Add(gc.configs[cacheName].ItemExpiration), true)
+ }
+ return cache.Set(ctx, key, value, expiration, true)
+ }
+ return CacheNotFoundError
+}
+
+// SetToExpireAt forces an item into the cache, to expire at a specific time regardless of the cache configuration. Use
+// SetContext if you need to set the expiration and a context.
+func (gc *Cache) SetToExpireAt(cacheName, key string, expireAt time.Time, value []byte) error {
+ gc.debugOut.Printf("Setting %s %s @ %s\n", cacheName, key, expireAt.String())
+ return gc.set(context.Background(), cacheName, key, value, expireAt)
+}
+
+// Remove makes a best effort to remove an item from the cache
+func (gc *Cache) Remove(cacheName, key string) error {
+ return gc.RemoveContext(context.Background(), cacheName, key)
+}
+
+// RemoveContext makes a best effort to remove an item from the cache, honoring the provided context.
+func (gc *Cache) RemoveContext(ctx context.Context, cacheName, key string) error {
+ if cache, ok := gc.caches[cacheName]; ok {
+ gc.debugOut.Printf("Removing %s %s\n", cacheName, key)
+ return cache.Remove(ctx, key)
+ }
+ return CacheNotFoundError
+}
+
+// SetDebugOut wires in the debug logger to the specified logger
+func (gc *Cache) SetDebugOut(logger *log.Logger) {
+ gc.debugOut = logger
+}
+
+// SetPeers allows the dynamic [re]setting of the peerlist
+func (gc *Cache) SetPeers(peers ...string) {
+ gc.pool.Set(peers...)
+}
+
+// Stats is a request finisher that outputs the Cache stats as JSON
+func (gc *Cache) Stats(w http.ResponseWriter, req *http.Request) {
+
+ stb, err := gc.stats()
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+
+ w.Write(stb)
+ w.Write([]byte{10})
+}
+
+func (gc *Cache) stats() ([]byte, error) {
+ type cachesStats struct {
+ Main groupcache.CacheStats
+ Hot groupcache.CacheStats
+ }
+ type stats struct {
+ Cache string
+ Group groupcache.Stats
+ Caches cachesStats
+ }
+
+ gc.regLock.Lock()
+ defer gc.regLock.Unlock()
+
+ statList := make([]stats, 0)
+
+ for name, gp := range gc.caches {
+ statList = append(statList,
+ stats{
+ Cache: name,
+ Group: gp.Stats,
+ Caches: cachesStats{
+ Main: gp.CacheStats(groupcache.MainCache),
+ Hot: gp.CacheStats(groupcache.HotCache),
+ },
+ })
+ }
+
+ data, err := json.MarshalIndent(statList, "", " ")
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+
+}
diff --git a/internal/caches/group/group_test.go b/internal/caches/group/group_test.go
new file mode 100644
index 00000000..01ed5e24
--- /dev/null
+++ b/internal/caches/group/group_test.go
@@ -0,0 +1,276 @@
+package group
+
+import (
+ . "github.com/smartystreets/goconvey/convey"
+
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+)
+
+func Test_GroupCache(t *testing.T) {
+
+ var Store = map[string][]byte{
+ "red": []byte("#FF0000"),
+ "green": []byte("#00FF00"),
+ "blue": []byte("#0000FF"),
+ }
+
+ // TIL you can't have multiple groupcache instances in the same running code
+ ports := []string{"http://127.0.0.1:8080"} //, "http://127.0.0.1:8081", "http://127.0.0.1:8082"}
+ f := func(key string) ([]byte, error) {
+ v, ok := Store[key]
+ if !ok {
+ return []byte{}, ItemNotFoundError
+ }
+ return v, nil
+ }
+
+ fp := func(key string) ([]byte, error) {
+ var pStore = map[string][]byte{
+ "black": []byte("#FFFFFF"),
+ }
+
+ v, ok := pStore[key]
+ if !ok {
+ return []byte{}, ItemNotFoundError
+ }
+ return v, nil
+ }
+
+ fg := func(key string) ([]byte, error) {
+ var gStore = map[string][]byte{
+ "white": []byte("#000000"),
+ }
+
+ v, ok := gStore[key]
+ if !ok {
+ return []byte{}, ItemNotFoundError
+ }
+ return v, nil
+ }
+
+ port := ports[0]
+ conf := Config{
+ CacheSize: 128 << 20,
+ Name: "default",
+ ListenAddress: port,
+ PeerList: ports,
+ //ItemExpiration: 1 * time.Second,
+ }
+
+ c, err := NewCache(conf, f)
+ if err != nil {
+ panic(err)
+ }
+ defer c.Close()
+
+ conf.Name = "passwd"
+ c.Add(conf, fp)
+ conf.Name = "group"
+ conf.ItemExpiration = 3 * time.Millisecond
+ c.Add(conf, fg)
+
+ Convey("When a new GroupCache is created it looks correct", t, func() {
+
+ names := c.Names()
+ So(names, ShouldContain, "passwd")
+ So(names, ShouldContain, "group")
+ So(c.Exists("passwd"), ShouldBeTrue)
+ So(c.Exists("group"), ShouldBeTrue)
+ So(c.Exists("nope"), ShouldBeFalse)
+
+ Convey("... Asking for existing and non-existing items works as expected.", func() {
+
+ for name, code := range Store {
+ color, ok := c.Get("default", name)
+ So(ok, ShouldBeTrue)
+ So(color, ShouldResemble, code)
+ }
+
+ _, ok := c.Get("default", "brown")
+ So(ok, ShouldBeFalse)
+ })
+
+ Convey("... Adding a new item, and then retrieving it works as expected.", func() {
+ black := []byte("#FFFFFF")
+ err := c.Set("default", "black", black)
+ So(err, ShouldBeNil)
+
+ color, ok := c.Get("default", "black")
+ So(ok, ShouldBeTrue)
+ So(color, ShouldResemble, black)
+
+ Convey("... Removing that, and then retrieving it works as expected.", func() {
+ c.Remove("default", "black")
+
+ _, ok = c.Get("default", "black")
+ So(ok, ShouldBeFalse)
+ })
+ })
+
+ Convey("... Adding a new item with an explicit expiration, and then retrieving it after expiration works as expected.", func() {
+ black := []byte("#FFFFFF")
+ err := c.SetToExpireAt("default", "black", time.Now().Add(5*time.Millisecond), black)
+ So(err, ShouldBeNil)
+
+ time.Sleep(10 * time.Millisecond)
+
+ color, ok := c.Get("default", "black")
+ So(ok, ShouldBeFalse)
+ So(color, ShouldEqual, ItemNotFoundError)
+
+ Convey("... Removing that, and then retrieving it works as expected.", func() {
+ c.Remove("default", "black")
+
+ _, ok = c.Get("default", "black")
+ So(ok, ShouldBeFalse)
+ })
+ })
+
+ Convey("... Asking for prefixed items, hits the prefix-specific BackFillFuncs", func() {
+ // Get passwd
+ pline, ok := c.Get("passwd", "black")
+ So(ok, ShouldBeTrue)
+ So(pline, ShouldResemble, []byte("#FFFFFF"))
+
+ // Get group
+ gline, ok := c.Get("group", "white")
+ So(ok, ShouldBeTrue)
+ So(gline, ShouldResemble, []byte("#000000"))
+
+ // Get a non-registered prefix
+ err, ok := c.Get("bogus", "white")
+ So(ok, ShouldBeFalse)
+ So(err, ShouldEqual, CacheNotFoundError)
+
+ Convey("... Overriding prefixed items works as expected, as does removing the override", func() {
+ // Override a group item
+ err := c.Set("group", "white", []byte("#FFFFFF"))
+ So(err, ShouldBeNil)
+ sline, ok := c.Get("group", "white")
+ So(ok, ShouldBeTrue)
+ So(sline, ShouldResemble, []byte("#FFFFFF"))
+
+ // Wait for the overridden group item to expire
+ time.Sleep(5 * time.Millisecond)
+ sline, ok = c.Get("group", "white")
+ So(ok, ShouldBeTrue)
+ So(sline, ShouldResemble, []byte("#000000")) // back to normal
+
+ })
+
+ Convey("... Setting and removing items from non-existent caches is as-expected", func() {
+
+ err := c.Set("nope", "white", []byte("#FFFFFF"))
+ So(err, ShouldEqual, CacheNotFoundError)
+
+ err = c.Remove("nope", "white")
+ So(err, ShouldEqual, CacheNotFoundError)
+ })
+ })
+
+ Convey("... Checking stats works as-expected", func() {
+ stb, err := c.stats()
+ So(err, ShouldBeNil)
+ x := make([]interface{}, 0)
+ jerr := json.Unmarshal(stb, &x)
+ So(jerr, ShouldBeNil)
+ So(x, ShouldNotBeEmpty)
+
+ })
+ })
+}
+
+func concatPrefixKey(prefix, sep, key string) string {
+ return prefix + sep + key
+}
+
+type ctxKey string
+
+const prefixKey = ctxKey("prefix")
+
+func Benchmark_Sprintf(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _ = fmt.Sprintf("%s%s%s", "stuff", "\t", "words")
+ }
+}
+
+func Benchmark_Concat(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _ = "stuff" + "\t" + "words"
+ }
+}
+
+func Benchmark_ConcatFunc(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _ = concatPrefixKey("stuff", "\t", "words")
+ }
+}
+
+func Benchmark_SplitNo(b *testing.B) {
+ key := "123"
+ sep := "\t"
+ for i := 0; i < b.N; i++ {
+ if p := strings.SplitN(key, sep, 2); len(p) > 1 {
+ _ = p
+ }
+ }
+}
+
+func Benchmark_SplitYes(b *testing.B) {
+ key := "456\t123"
+ sep := "\t"
+ for i := 0; i < b.N; i++ {
+ if p := strings.SplitN(key, sep, 2); len(p) > 1 {
+ _ = p
+ }
+ }
+}
+
+func Benchmark_SplitHasNo(b *testing.B) {
+ key := "123"
+ sep := "\t"
+ for i := 0; i < b.N; i++ {
+ if strings.Contains(key, sep) {
+ _ = strings.SplitN(key, sep, 2)
+ }
+ }
+}
+
+func Benchmark_SplitHasYes(b *testing.B) {
+ key := "456\t123"
+ sep := "\t"
+ for i := 0; i < b.N; i++ {
+ if strings.Contains(key, sep) {
+ _ = strings.SplitN(key, sep, 2)
+ }
+ }
+}
+
+func Benchmark_SplitContextNo(b *testing.B) {
+ key := "123"
+ sep := "\t"
+ ctx := context.Background()
+ for i := 0; i < b.N; i++ {
+ if x := ctx.Value(prefixKey); x != nil {
+ prefix := x.(string)
+ key = strings.TrimPrefix(key, prefix+sep)
+ }
+ }
+}
+
+func Benchmark_SplitContextYes(b *testing.B) {
+ key := "456\t123"
+ sep := "\t"
+ ctx := context.WithValue(context.Background(), prefixKey, "456")
+ for i := 0; i < b.N; i++ {
+ if x := ctx.Value("prefix"); x != nil {
+ prefix := x.(string)
+ key = strings.TrimPrefix(key, prefix+sep)
+ }
+ }
+}
diff --git a/internal/caches/group/misc.go b/internal/caches/group/misc.go
new file mode 100644
index 00000000..934eedb2
--- /dev/null
+++ b/internal/caches/group/misc.go
@@ -0,0 +1,22 @@
+package group
+
+import (
+ "time"
+)
+
+// Error is an error type
+type Error string
+
+// Error returns the stringified version of Error
+func (e Error) Error() string {
+ return string(e)
+}
+
+// Config is used to store configuration information to pass to a GroupCache.
+type Config struct {
+ Name string // For New and Add. Pass as ``cacheName`` to differentiate caches
+ ListenAddress string // Only for New to set the listener
+ PeerList []string // Only for New to establish the initial PeerList. May be reset with GroupCache.SetPeers()
+ CacheSize int64 // For New and Add to set the size in bytes of the cache
+ ItemExpiration time.Duration // For New and Add to set the default expiration duration. Leave as empty for infinite.
+}