├── .github └── workflows │ └── go-test.yml ├── README.md ├── fastcache.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── stores ├── goredis │ ├── go.mod │ ├── go.sum │ ├── pool.go │ ├── redis-cluster-docker-compose.yml │ ├── redis.go │ ├── redis_cluster_test.go │ └── redis_test.go └── redis │ ├── go.mod │ ├── go.sum │ ├── pool.go │ └── redis.go └── tests ├── fastcache_test.go ├── go.mod └── go.sum /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | # Triggers the workflow on push or pull request events 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go: ["1.18", "1.20"] 11 | 12 | runs-on: ubuntu-20.04 13 | 14 | name: Go ${{ matrix.go }} Tests 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Run Test 24 | run: go test -v github.com/zerodha/fastcache... 25 | 26 | - name: Run Coverage 27 | run: go test -v -cover github.com/zerodha/fastcache... 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastcache 2 | 3 | fastcache is a simple response caching package that plugs into [fastglue](https://github.com/zerodha/fastglue). The 4 | `Cached()` middleware can be wrapped around fastglue GET handlers that need to serve 5 | cached bytes for a request. 6 | 7 | The `ClearGroup()` handler is meant for invalidating cache, for 8 | wrapping POST / PUT / DELETE handlers. It supports arbitrary backend storage implementations and ships with redigo/go-redis store implementations. 9 | 10 | ## Concepts 11 | 12 | #### `namespace (fastcache.Options.NamespaceKey)`. 13 | 14 | All cached items are saved under a namespace. For authenticated calls, this is most commonly the user ID. So, all GET requests for a user XX1234 like orders, marketwatch, profile etc. may be namespaced under a XX1234 key in the store. 15 | 16 | `fastcache.Options.NamespaceKey` is the name of the key that'll have the name of the namespace in a `RequestCtx.UserValue(NamespaceKey)`. This UserValue should be set by another middleware, such as the auth middleware, before the `Cached()` middleware is executed. For example, `RequestCtx.SetUerValue("user_id", "XX1234")` where `user_id` is the NamespaceKey. For handlers with params like `/orders/:user_id`, this is taken care of by the router. 17 | 18 | ### `group` 19 | 20 | Cache for different URIs of the same type can be grouped under a single name so that the cache for a group can be deleted in one go when something changes. For instance, orders and tradebook handlers can be grouped "orders" and different marketwatch calls can be grouped under "mw". 21 | 22 | ## Middlewares 23 | 24 | `Cached()` is the middleware for GET calls that does caching, 304 serving etc. 25 | 26 | `ClearGroup()` is middleware handlers for POST / PUT / DELETE methods that are meant to clear cache for GET calls. 27 | 28 | ## Manual cache clearing 29 | 30 | The `.Del()` and `.DelGroup()` can be used to manually clear cached items when handler based clearing isn't sufficient. 31 | 32 | ## Example 33 | ```shell 34 | # Install fastcache. 35 | go get -u github.com/zerodha/fastcache/v4 36 | 37 | # Install a store. Stores are separate packages themselves. 38 | go get -u github.com/zerodha/fastcache/stores/goredis/v9 39 | ``` 40 | 41 | 42 | ```go 43 | 44 | fc := fastcache.New(redis.New(pool)) 45 | 46 | // Long cache options. 47 | long := &fastcache.Options{ 48 | NamespaceKey: "user_id", 49 | ETag: true, 50 | } 51 | short := &fastcache.Options{ 52 | NamespaceKey: "user_id", 53 | TTL: time.Second * 60, 54 | ETag: true, 55 | } 56 | 57 | g.GET("/margins", auth(fc.Cached(handleGetMargins, short, "margins"))) 58 | g.GET("/margins/:segment", auth(fc.Cached(handleGetMargins, short, "margins"))) 59 | 60 | g.GET("/orders", auth(fc.Cached(handleGetMargins, long, "orders"))) 61 | g.GET("/orders", auth(fc.Cached(handleGetMargins, short, "trades"))) 62 | 63 | // Clear the orders group. Multiple groups can be specified like: orders, positions ... 64 | g.DELETE("/orders/:order_id",auth(app.respcache.ClearGroup(handleDeleteMarketwatchItems, short, []string{"orders"}))) 65 | ``` 66 | 67 | ## Dev Notes 68 | 69 | ### Running Tests 70 | 71 | ```shell 72 | go test -v github.com/zerodha/fastcache... 73 | ``` 74 | 75 | ### Running Cluster Tests 76 | 77 | Cluster tests require testcontainers (and docker) to run redis containers. 78 | 79 | Therefore, we have kept the cluster tests in a separate file and 80 | they are tagged with `clustertest`. If you want to run the cluster tests, 81 | you need to have docker installed and running. 82 | 83 | 84 | 85 | 86 | After that you can simply run the tests with the following command: 87 | 88 | ```shell 89 | go test -tags clustertest -v github.com/zerodha/fastcache... 90 | ``` 91 | 92 | We also provide a `redis-cluster-docker-compose.yml` file that can be 93 | used to start a redis cluster for local testing. 94 | 95 | ```shell 96 | docker-compose -f redis-cluster-docker-compose.yml up -d 97 | ``` -------------------------------------------------------------------------------- /fastcache.go: -------------------------------------------------------------------------------- 1 | // Package fastcache provides a simple HTTP response caching layer that can 2 | // be plugged into fastglue. 3 | package fastcache 4 | 5 | import ( 6 | "bytes" 7 | "compress/gzip" 8 | "crypto/md5" 9 | "crypto/rand" 10 | "encoding/hex" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "strings" 16 | "time" 17 | 18 | "github.com/valyala/fasthttp" 19 | "github.com/zerodha/fastglue" 20 | ) 21 | 22 | // FastCache is the cache controller. 23 | type FastCache struct { 24 | s Store 25 | } 26 | 27 | // CompressionsOptions defines gzip compression options. 28 | type CompressionsOptions struct { 29 | // Enabled causes all blobs to be compressed before writing to the store, as long 30 | // as the blog is of MinLength length. 31 | Enabled bool 32 | 33 | // MinLength is the minimum number of bytes in the response which causes it to be compressed 34 | // before being stored. Default is 500 bytes. 35 | MinLength int 36 | 37 | // If RespectHeaders is true, then `Accept-encoding` header is considered and an 38 | // appropriate blob, compressed or uncompressed is returned. When set to false, 39 | // the stored response is always decompressed and the resultant decompressed data is served. 40 | RespectHeaders bool 41 | } 42 | 43 | // Options has FastCache options. 44 | type Options struct { 45 | // namespaceKey is the namespace that is used to namespace and store cache values. 46 | // The value of the key is obtained from RequestCtx.UserValue(namespaceKey). 47 | // This should be set by a middleware (such as auth) before the cache 48 | // middleware is called. For authenticated calls, this is most commonly 49 | // be a user id so that all URIs for a particular user are cached under 50 | // the user's namespace. 51 | NamespaceKey string 52 | 53 | // TTL for a cache item. If this is not set, no TTL is applied to cached 54 | // items. 55 | TTL time.Duration 56 | 57 | // Process ETags and send 304s? 58 | ETag bool 59 | 60 | // By default, handler response bodies are cached and served. If this is 61 | // enabled, only ETags are cached and for response bodies, the original 62 | // handler is invoked. 63 | NoBlob bool 64 | 65 | // Logger is the optional logger to which errors will be written. 66 | Logger *log.Logger 67 | 68 | // Cache based on uri+querystring. 69 | IncludeQueryString bool 70 | 71 | Compression CompressionsOptions 72 | } 73 | 74 | // Item represents the cache entry for a single endpoint with the actual cache 75 | // body and metadata. 76 | type Item struct { 77 | ContentType string 78 | Compression string 79 | ETag string 80 | // If the Blob is used beyond the scope of the request, it should be copied. 81 | // Such as when the cache is written asynchronously. 82 | Blob []byte 83 | } 84 | 85 | // Store represents a backend data store where bytes are cached. Individual 86 | // keys are namespaced under 87 | type Store interface { 88 | Get(namespace, group, uri string) (Item, error) 89 | Put(namespace, group, uri string, b Item, ttl time.Duration) error 90 | Del(namespace, group, uri string) error 91 | DelGroup(namespace string, group ...string) error 92 | } 93 | 94 | const compGzip = "gzip" 95 | 96 | var cacheNoStore = []byte("no-store") 97 | 98 | // New creates and returns a new FastCache instance. 99 | func New(s Store) *FastCache { 100 | return &FastCache{ 101 | s: s, 102 | } 103 | } 104 | 105 | // Cached middleware "dumb" caches 200 HTTP responses as bytes with an optional TTL. 106 | // This is used to wrap GET calls that need response cache. 107 | // 108 | // In addition to retrieving / caching HTTP responses, it also accepts 109 | // ETags from clients and sends a 304 response with no actual body 110 | // in case there's an ETag match. 111 | // 112 | // group is the name for the group of requests. For instance, all the GET 113 | // requests for orders can have the group "orders" so that they can be cleared 114 | // in one shot when something changes using the Del*() methods or Clear*() middleware. 115 | func (f *FastCache) Cached(h fastglue.FastRequestHandler, o *Options, group string) fastglue.FastRequestHandler { 116 | if o.Logger == nil { 117 | o.Logger = log.New(io.Discard, "", 0) 118 | } 119 | 120 | return func(r *fastglue.Request) error { 121 | namespace, _ := r.RequestCtx.UserValue(o.NamespaceKey).(string) 122 | if namespace == "" { 123 | o.Logger.Printf("no namespace found in UserValue() for key '%s'", o.NamespaceKey) 124 | return h(r) 125 | } 126 | 127 | if o.Compression.Enabled && o.Compression.MinLength < 1 { 128 | o.Compression.MinLength = 500 129 | } 130 | 131 | var hash [16]byte 132 | // If IncludeQueryString option is set then cache based on uri + md5(query_string) 133 | if o.IncludeQueryString { 134 | hash = md5.Sum(r.RequestCtx.URI().FullURI()) 135 | } else { 136 | hash = md5.Sum(r.RequestCtx.URI().Path()) 137 | } 138 | uri := hex.EncodeToString(hash[:]) 139 | 140 | // Fetch etag + cached bytes from the store. 141 | blob, err := f.s.Get(namespace, group, uri) 142 | if err != nil { 143 | o.Logger.Printf("error reading cache: %v", err) 144 | } 145 | 146 | // If ETag matching is enabled, attempt to match the header etag 147 | // with the stored one (if there's any). 148 | if o.ETag { 149 | var ( 150 | match = string(r.RequestCtx.Request.Header.Peek("If-None-Match")) 151 | ) 152 | if len(match) > 4 && len(blob.ETag) > 0 && strings.Contains(match, blob.ETag) { 153 | r.RequestCtx.SetStatusCode(fasthttp.StatusNotModified) 154 | return nil 155 | } 156 | } 157 | 158 | // There's cache. Write it and end the request. 159 | if len(blob.Blob) > 0 { 160 | if o.ETag { 161 | r.RequestCtx.Response.Header.Add("ETag", `"`+string(blob.ETag)+`"`) 162 | } 163 | r.RequestCtx.SetStatusCode(fasthttp.StatusOK) 164 | r.RequestCtx.SetContentType(blob.ContentType) 165 | 166 | out := blob.Blob 167 | 168 | // Compression is enabled. 169 | if o.Compression.Enabled && blob.Compression == compGzip { 170 | // Header is requesting for gzipped content. 171 | if o.Compression.RespectHeaders && r.RequestCtx.Request.Header.HasAcceptEncoding(compGzip) { 172 | r.RequestCtx.Request.Header.Set("Content-Encoding", compGzip) 173 | } else { 174 | // Decompress the compressed blob and send uncompressed response. 175 | b, err := decompressGzip(out) 176 | if err != nil { 177 | o.Logger.Printf("error decompressing blob: %v", err) 178 | } 179 | out = b 180 | } 181 | } 182 | 183 | if _, err := r.RequestCtx.Write(out); err != nil { 184 | o.Logger.Printf("error writing request: %v", err) 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // Execute the actual handler. 191 | if err := h(r); err != nil { 192 | o.Logger.Printf("error running middleware: %v", err) 193 | } 194 | 195 | // Read the response body written by the handler and cache it. 196 | if r.RequestCtx.Response.StatusCode() == 200 { 197 | // If "no-store" is set in the cache control header, don't cache. 198 | if !bytes.Contains(r.RequestCtx.Response.Header.Peek("Cache-Control"), cacheNoStore) { 199 | if err := f.cache(r, namespace, group, o); err != nil { 200 | o.Logger.Println(err.Error()) 201 | } 202 | } 203 | } 204 | return nil 205 | } 206 | } 207 | 208 | // ClearGroup middleware clears cache set by the Cached() middleware 209 | // for the all the specified groups. 210 | // 211 | // This should ideally wrap write handlers (POST / PUT / DELETE) 212 | // and the cache is cleared when the handler responds with a 200. 213 | func (f *FastCache) ClearGroup(h fastglue.FastRequestHandler, o *Options, groups ...string) fastglue.FastRequestHandler { 214 | if o.Logger == nil { 215 | o.Logger = log.New(ioutil.Discard, "", 0) 216 | } 217 | 218 | return func(r *fastglue.Request) error { 219 | namespace, _ := r.RequestCtx.UserValue(o.NamespaceKey).(string) 220 | if namespace == "" { 221 | o.Logger.Printf("no namespace found in UserValue() for key '%s'", o.NamespaceKey) 222 | return h(r) 223 | } 224 | 225 | // Execute the actual handler. 226 | if err := h(r); err != nil { 227 | o.Logger.Printf("error running middleware: %v", err) 228 | } 229 | 230 | // Clear cache. 231 | if r.RequestCtx.Response.StatusCode() == 200 { 232 | if err := f.DelGroup(namespace, groups...); err != nil { 233 | o.Logger.Printf("error while deleting groups '%v': %v", groups, err) 234 | } 235 | } 236 | return nil 237 | } 238 | } 239 | 240 | // Del deletes the cache for a single URI in a namespace->group. 241 | func (f *FastCache) Del(namespace, group, uri string) error { 242 | return f.s.Del(namespace, group, uri) 243 | } 244 | 245 | // DelGroup deletes all cached URIs under a group. 246 | func (f *FastCache) DelGroup(namespace string, group ...string) error { 247 | return f.s.DelGroup(namespace, group...) 248 | } 249 | 250 | // cache caches a response body. 251 | func (f *FastCache) cache(r *fastglue.Request, namespace, group string, o *Options) error { 252 | // ETag?. 253 | var etag string 254 | if o.ETag { 255 | e, err := generateRandomString(16) 256 | if err != nil { 257 | return fmt.Errorf("error generating etag: %v", err) 258 | } 259 | etag = e 260 | } 261 | 262 | // Write cache to the store (etag, content type, response body). 263 | var hash [16]byte 264 | // If IncludeQueryString option is set then cache based on uri + md5(query_string) 265 | if o.IncludeQueryString { 266 | hash = md5.Sum(r.RequestCtx.URI().FullURI()) 267 | } else { 268 | hash = md5.Sum(r.RequestCtx.URI().Path()) 269 | } 270 | uri := hex.EncodeToString(hash[:]) 271 | 272 | var blob []byte 273 | if !o.NoBlob { 274 | blob = r.RequestCtx.Response.Body() 275 | } 276 | 277 | item := Item{ 278 | ETag: etag, 279 | ContentType: string(r.RequestCtx.Response.Header.ContentType()), 280 | Blob: blob, 281 | } 282 | 283 | // Optionally compress the response. 284 | if o.Compression.Enabled && len(blob) >= o.Compression.MinLength { 285 | b, err := compressGzip(blob) 286 | if err != nil { 287 | o.Logger.Printf("error compressing blob: %v", err) 288 | } else { 289 | item.Blob = b 290 | item.Compression = compGzip 291 | } 292 | } 293 | 294 | err := f.s.Put(namespace, group, uri, item, o.TTL) 295 | if err != nil { 296 | return fmt.Errorf("error writing cache to store: %v", err) 297 | } 298 | 299 | // Send the eTag with the response. 300 | if o.ETag { 301 | r.RequestCtx.Response.Header.Add("ETag", `"`+string(etag)+`"`) 302 | } 303 | return nil 304 | } 305 | 306 | // generateRandomString generates a cryptographically random, 307 | // alphanumeric string of length n. 308 | func generateRandomString(totalLen int) (string, error) { 309 | const dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 310 | var ( 311 | bytes = make([]byte, totalLen) 312 | ) 313 | if _, err := rand.Read(bytes); err != nil { 314 | return "", err 315 | } 316 | 317 | for k, v := range bytes { 318 | bytes[k] = dictionary[v%byte(len(dictionary))] 319 | } 320 | return string(bytes), nil 321 | } 322 | 323 | func compressGzip(b []byte) ([]byte, error) { 324 | var buf bytes.Buffer 325 | 326 | w := gzip.NewWriter(&buf) 327 | if _, err := w.Write(b); err != nil { 328 | return nil, err 329 | } 330 | w.Close() 331 | 332 | return buf.Bytes(), nil 333 | } 334 | 335 | func decompressGzip(b []byte) ([]byte, error) { 336 | r, err := gzip.NewReader(bytes.NewReader(b)) 337 | if err != nil { 338 | return nil, err 339 | } 340 | defer r.Close() 341 | 342 | return io.ReadAll(r) 343 | } 344 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerodha/fastcache/v4 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/valyala/fasthttp v1.34.0 7 | github.com/zerodha/fastglue v1.7.1 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.4 // indirect 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/fasthttp/router v1.4.5 // indirect 14 | github.com/klauspost/compress v1.15.0 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect 17 | github.com/stretchr/testify v1.6.1 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 2 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 3 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fasthttp/router v1.4.5 h1:YZonsKCssEwEi3veDMhL6okIx550qegAiuXAK8NnM3Y= 8 | github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXhaTRDG9Y= 9 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 12 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 13 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= 14 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI= 18 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 22 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 24 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 25 | github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= 26 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 27 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 28 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 29 | github.com/zerodha/fastglue v1.7.1 h1:YbKiSSEYzDmVDM29KCeXMHuh+48TcEHsgYCIGjoUcbU= 30 | github.com/zerodha/fastglue v1.7.1/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= 31 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 32 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 33 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 34 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 35 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 36 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 44 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 45 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 46 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 47 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 48 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.18 2 | 3 | use ( 4 | . 5 | ./stores/redis 6 | ./stores/goredis 7 | ./tests 8 | ) 9 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 2 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 4 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 5 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 6 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 7 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 8 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 9 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 10 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 11 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 12 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 13 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 14 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 15 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 17 | -------------------------------------------------------------------------------- /stores/goredis/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerodha/fastcache/stores/goredis/v9 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alicebob/miniredis/v2 v2.33.0 7 | github.com/redis/go-redis/v9 v9.5.1 8 | github.com/stretchr/testify v1.9.0 9 | github.com/testcontainers/testcontainers-go v0.33.0 10 | github.com/zerodha/fastcache/v4 v4.0.0 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 16 | github.com/Microsoft/go-winio v0.6.2 // indirect 17 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect 18 | github.com/andybalholm/brotli v1.0.4 // indirect 19 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 21 | github.com/containerd/containerd v1.7.18 // indirect 22 | github.com/containerd/log v0.1.0 // indirect 23 | github.com/containerd/platforms v0.2.1 // indirect 24 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 27 | github.com/distribution/reference v0.6.0 // indirect 28 | github.com/docker/docker v27.1.1+incompatible // indirect 29 | github.com/docker/go-connections v0.5.0 // indirect 30 | github.com/docker/go-units v0.5.0 // indirect 31 | github.com/fasthttp/router v1.4.5 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/go-logr/logr v1.4.1 // indirect 34 | github.com/go-logr/stdr v1.2.2 // indirect 35 | github.com/go-ole/go-ole v1.2.6 // indirect 36 | github.com/gogo/protobuf v1.3.2 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/klauspost/compress v1.17.4 // indirect 39 | github.com/kr/text v0.2.0 // indirect 40 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 41 | github.com/magiconair/properties v1.8.7 // indirect 42 | github.com/moby/docker-image-spec v1.3.1 // indirect 43 | github.com/moby/patternmatcher v0.6.0 // indirect 44 | github.com/moby/sys/sequential v0.5.0 // indirect 45 | github.com/moby/sys/user v0.1.0 // indirect 46 | github.com/moby/term v0.5.0 // indirect 47 | github.com/morikuni/aec v1.0.0 // indirect 48 | github.com/opencontainers/go-digest v1.0.0 // indirect 49 | github.com/opencontainers/image-spec v1.1.0 // indirect 50 | github.com/pkg/errors v0.9.1 // indirect 51 | github.com/pmezard/go-difflib v1.0.0 // indirect 52 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 53 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect 54 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 55 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 56 | github.com/sirupsen/logrus v1.9.3 // indirect 57 | github.com/tklauser/go-sysconf v0.3.12 // indirect 58 | github.com/tklauser/numcpus v0.6.1 // indirect 59 | github.com/valyala/bytebufferpool v1.0.0 // indirect 60 | github.com/valyala/fasthttp v1.34.0 // indirect 61 | github.com/yuin/gopher-lua v1.1.1 // indirect 62 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 63 | github.com/zerodha/fastglue v1.7.1 // indirect 64 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 65 | go.opentelemetry.io/otel v1.24.0 // indirect 66 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 67 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 68 | golang.org/x/crypto v0.35.0 // indirect 69 | golang.org/x/sys v0.30.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /stores/goredis/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 5 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 6 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 7 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 8 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= 9 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 10 | github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= 11 | github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= 12 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 13 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 14 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 15 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 16 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 17 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 18 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 19 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 20 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 21 | github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= 22 | github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= 23 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 24 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 25 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 26 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 27 | github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= 28 | github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 29 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 30 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 31 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 35 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 36 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 37 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 38 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 39 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 40 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 41 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 42 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 43 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 44 | github.com/fasthttp/router v1.4.5 h1:YZonsKCssEwEi3veDMhL6okIx550qegAiuXAK8NnM3Y= 45 | github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXhaTRDG9Y= 46 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 47 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 48 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 49 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 50 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 51 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 52 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 53 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 54 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 55 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 56 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 57 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 58 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 60 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 61 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 62 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 63 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 64 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 65 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 66 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 67 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 68 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 69 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 70 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 71 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 72 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 73 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 74 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 75 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 76 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 77 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 78 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 79 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 80 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 81 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 82 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 83 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 84 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 85 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 86 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 87 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 88 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 89 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 90 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 91 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 92 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 93 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 94 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 95 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 96 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 97 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 98 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 99 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 100 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 101 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 102 | github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= 103 | github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 104 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 105 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI= 106 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= 107 | github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= 108 | github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= 109 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 110 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 111 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 112 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 113 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 114 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 115 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 116 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 117 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 118 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 120 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 121 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 122 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 123 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 124 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 125 | github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= 126 | github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= 127 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 128 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 129 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 130 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 131 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 132 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 133 | github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= 134 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 135 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 136 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 137 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 138 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 139 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 140 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 141 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 142 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 143 | github.com/zerodha/fastcache/v4 v4.0.0 h1:cxJ2AwLAxxvWiarB2/P+PEWAU6/ZfNY3bkBkWXzQKbs= 144 | github.com/zerodha/fastcache/v4 v4.0.0/go.mod h1:jfFLkiuyMIO8u7KlojdeLGaVyPuVXES0PRGxS7byr6s= 145 | github.com/zerodha/fastglue v1.7.1 h1:YbKiSSEYzDmVDM29KCeXMHuh+48TcEHsgYCIGjoUcbU= 146 | github.com/zerodha/fastglue v1.7.1/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= 147 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 148 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 149 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 150 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 151 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 152 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 153 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 154 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 155 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 156 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 157 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 158 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 159 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 160 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 161 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 162 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 163 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 164 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 165 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 166 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 167 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 168 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 169 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 171 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 172 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 173 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 174 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 175 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 176 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 177 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 179 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 181 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 194 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 196 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 197 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 198 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 199 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 200 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 201 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 202 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 203 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 204 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 205 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 206 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 207 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 208 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 209 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 210 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 211 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 213 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 214 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 215 | google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= 216 | google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= 217 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= 218 | google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= 219 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 220 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 221 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 222 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 223 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 224 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 225 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 226 | -------------------------------------------------------------------------------- /stores/goredis/pool.go: -------------------------------------------------------------------------------- 1 | package goredis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | // NewPool returns a Redigo cachepool. 10 | func NewPool(address string, password string, db int, maxActiv int, maxIdle int, timeout time.Duration) *redis.Client { 11 | return redis.NewClient(&redis.Options{ 12 | Addr: address, 13 | Password: password, 14 | DB: db, 15 | PoolSize: maxActiv, 16 | ReadTimeout: timeout, 17 | WriteTimeout: timeout, 18 | MinIdleConns: maxIdle, 19 | MinRetryBackoff: 500 * time.Millisecond, 20 | MaxRetryBackoff: 2000 * time.Millisecond, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /stores/goredis/redis-cluster-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | redis-node-0: 4 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 5 | ports: 6 | - 6379:6379 7 | volumes: 8 | - redis-cluster_data-0:/bitnami/redis/data 9 | environment: 10 | - 'ALLOW_EMPTY_PASSWORD=yes' 11 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 12 | networks: 13 | - kite-docker 14 | 15 | redis-node-1: 16 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 17 | ports: 18 | - 6380:6379 19 | volumes: 20 | - redis-cluster_data-1:/bitnami/redis/data 21 | environment: 22 | - 'ALLOW_EMPTY_PASSWORD=yes' 23 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 24 | networks: 25 | - kite-docker 26 | 27 | redis-node-2: 28 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 29 | ports: 30 | - 6381:6379 31 | volumes: 32 | - redis-cluster_data-2:/bitnami/redis/data 33 | environment: 34 | - 'ALLOW_EMPTY_PASSWORD=yes' 35 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 36 | networks: 37 | - kite-docker 38 | 39 | redis-node-3: 40 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 41 | ports: 42 | - 6382:6379 43 | volumes: 44 | - redis-cluster_data-3:/bitnami/redis/data 45 | environment: 46 | - 'ALLOW_EMPTY_PASSWORD=yes' 47 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 48 | networks: 49 | - kite-docker 50 | 51 | redis-node-4: 52 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 53 | ports: 54 | - 6383:6379 55 | volumes: 56 | - redis-cluster_data-4:/bitnami/redis/data 57 | environment: 58 | - 'ALLOW_EMPTY_PASSWORD=yes' 59 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 60 | networks: 61 | - kite-docker 62 | 63 | redis-node-5: 64 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 65 | ports: 66 | - 6384:6379 67 | volumes: 68 | - redis-cluster_data-5:/bitnami/redis/data 69 | environment: 70 | - 'ALLOW_EMPTY_PASSWORD=yes' 71 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 72 | networks: 73 | - kite-docker 74 | 75 | redis-cluster-init: 76 | image: docker.io/bitnami/redis-cluster:6.0-debian-10 77 | depends_on: 78 | - redis-node-0 79 | - redis-node-1 80 | - redis-node-2 81 | - redis-node-3 82 | - redis-node-4 83 | - redis-node-5 84 | environment: 85 | - 'ALLOW_EMPTY_PASSWORD=yes' 86 | - 'REDIS_CLUSTER_REPLICAS=1' 87 | - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' 88 | - 'REDIS_CLUSTER_CREATOR=yes' 89 | networks: 90 | - kite-docker 91 | 92 | volumes: 93 | redis-cluster_data-0: 94 | driver: local 95 | redis-cluster_data-1: 96 | driver: local 97 | redis-cluster_data-2: 98 | driver: local 99 | redis-cluster_data-3: 100 | driver: local 101 | redis-cluster_data-4: 102 | driver: local 103 | redis-cluster_data-5: 104 | driver: local 105 | 106 | networks: 107 | kite-docker: 108 | ipam: 109 | driver: default 110 | config: 111 | - subnet: 172.30.0.0/16 112 | 113 | -------------------------------------------------------------------------------- /stores/goredis/redis.go: -------------------------------------------------------------------------------- 1 | // Package goredis implements a Redis cache storage backend for fastcache. 2 | // The internal structure looks like this where 3 | // XX1234 = namespace, marketwach = group 4 | // ``` 5 | // 6 | // CACHE:XX1234:marketwatch { 7 | // "/user/marketwatch_ctype" -> []byte 8 | // "/user/marketwatch_etag" -> []byte 9 | // "/user/marketwatch_blob" -> []byte 10 | // "/user/marketwatch/123_ctype" -> []byte 11 | // "/user/marketwatch/123_etag" -> []byte 12 | // "/user/marketwatch/123_blob" -> []byte 13 | // } 14 | // 15 | // ``` 16 | // 17 | // This library also supports async mode which is dependent on the go-redis 18 | // library. ref: 19 | // https://github.com/redis/go-redis/discussions/2597#discussioncomment-5909650 20 | // 21 | // ``` 22 | package goredis 23 | 24 | import ( 25 | "context" 26 | "errors" 27 | "io" 28 | "log" 29 | "time" 30 | "unsafe" 31 | 32 | "github.com/redis/go-redis/v9" 33 | "github.com/zerodha/fastcache/v4" 34 | ) 35 | 36 | const ( 37 | // Store keys. 38 | keyEtag = "_etag" 39 | keyCtype = "_ctype" 40 | keyCompression = "_comp" 41 | keyBlob = "_blob" 42 | 43 | sep = ":" 44 | ) 45 | 46 | // Store is a Redis cache store implementation for fastcache. 47 | type Store struct { 48 | config Config 49 | putBuf chan putReq 50 | cn redis.UniversalClient 51 | ctx context.Context 52 | logger *log.Logger 53 | } 54 | 55 | type Config struct { 56 | // Prefix is the prefix to apply to all cache keys. 57 | // Note: in async mode you can use braces to specify the {sharding_key}. 58 | Prefix string 59 | 60 | // Async enables async writes to Redis. If enabled, writes are buffered 61 | // and committed in batches. 62 | Async bool 63 | // AsyncMaxCommitSize is the maximum number of writes to commit in a single 64 | // batch. 65 | AsyncMaxCommitSize int 66 | // AsyncBufSize is the size of the write buffer, i.e. the channel size for 67 | // async writes. If the buffer is full, writes will block; so make sure to 68 | // set this to a reasonable value, ideally higher than maxCommitSize. 69 | AsyncBufSize int 70 | // AsyncCommitFreq is the time to wait before committing the write 71 | // buffer. 72 | AsyncCommitFreq time.Duration 73 | // Logger is an optional logger to which errors will be written. If it is 74 | // nil, errors are sent to io.Discard. 75 | Logger *log.Logger 76 | } 77 | 78 | // New creates a new Redis instance. prefix is the prefix to apply to all 79 | // cache keys. 80 | func New(cfg Config, client redis.UniversalClient) *Store { 81 | s := &Store{ 82 | config: cfg, 83 | cn: client, 84 | logger: cfg.Logger, 85 | ctx: context.TODO(), 86 | } 87 | 88 | if s.logger == nil { 89 | s.logger = log.New(io.Discard, "", 0) 90 | } 91 | 92 | // Start the async worker if enabled. 93 | if cfg.Async { 94 | // Set defaults. 95 | if s.config.AsyncBufSize == 0 { 96 | s.config.AsyncBufSize = 1000 97 | } 98 | 99 | if s.config.AsyncMaxCommitSize == 0 { 100 | s.config.AsyncMaxCommitSize = 100 101 | } 102 | 103 | if s.config.AsyncCommitFreq == 0 { 104 | s.config.AsyncCommitFreq = 100 * time.Millisecond 105 | } 106 | 107 | s.putBuf = make(chan putReq, s.config.AsyncBufSize) 108 | go s.putWorker() 109 | } 110 | 111 | return s 112 | } 113 | 114 | // Get gets the fastcache.Item for a single cached URI. 115 | func (s *Store) Get(namespace, group, uri string) (fastcache.Item, error) { 116 | var ( 117 | out fastcache.Item 118 | ) 119 | // Get content_type, etag, blob in that order. 120 | cmd := s.cn.HMGet(s.ctx, s.key(namespace, group), s.field(keyCtype, uri), s.field(keyEtag, uri), s.field(keyCompression, uri), s.field(keyBlob, uri)) 121 | if err := cmd.Err(); err != nil { 122 | return out, err 123 | } 124 | 125 | resp, err := cmd.Result() 126 | if err != nil { 127 | return out, err 128 | } 129 | 130 | if resp[0] == nil || resp[1] == nil || resp[2] == nil { 131 | return out, errors.New("goredis-store: nil received") 132 | } 133 | 134 | if ctype, ok := resp[0].(string); ok { 135 | out.ContentType = ctype 136 | } else { 137 | return out, errors.New("goredis-store: invalid type received for ctype") 138 | } 139 | 140 | if etag, ok := resp[1].(string); ok { 141 | out.ETag = etag 142 | } else { 143 | return out, errors.New("goredis-store: invalid type received for etag") 144 | } 145 | 146 | if comp, ok := resp[2].(string); ok { 147 | out.Compression = comp 148 | } else { 149 | return out, errors.New("goredis-store: invalid type received for etag") 150 | } 151 | 152 | if blob, ok := resp[3].(string); ok { 153 | out.Blob = stringToBytes(blob) 154 | } else { 155 | return out, errors.New("goredis-store: invalid type received for blob") 156 | } 157 | 158 | return out, err 159 | } 160 | 161 | type putReq struct { 162 | namespace string 163 | group string 164 | uri string 165 | b fastcache.Item 166 | ttl time.Duration 167 | } 168 | 169 | // Put sets a value to given session but stored only on commit 170 | func (s *Store) Put(namespace, group, uri string, b fastcache.Item, ttl time.Duration) error { 171 | if s.config.Async { 172 | // In async mode, we need to copy b.Blob to prevent fasthttp from reusing 173 | // the buffer, as we will use the buffer in a separate goroutine beyond 174 | // the scope of the current request. 175 | blobCopy := make([]byte, len(b.Blob)) 176 | copy(blobCopy, b.Blob) 177 | b.Blob = blobCopy 178 | 179 | // Send the put request to the async buffer channel. 180 | s.putBuf <- putReq{namespace, group, uri, b, ttl} 181 | return nil 182 | } 183 | 184 | return s.putSync(namespace, group, uri, b, ttl) 185 | } 186 | 187 | func (s *Store) putSync(namespace, group, uri string, b fastcache.Item, ttl time.Duration) error { 188 | var ( 189 | key = s.key(namespace, group) 190 | p = s.cn.Pipeline() 191 | ) 192 | 193 | if err := p.HMSet(s.ctx, key, map[string]interface{}{ 194 | s.field(keyCtype, uri): b.ContentType, 195 | s.field(keyEtag, uri): b.ETag, 196 | s.field(keyCompression, uri): b.Compression, 197 | s.field(keyBlob, uri): b.Blob, 198 | }).Err(); err != nil { 199 | return err 200 | } 201 | 202 | // Set a TTL for the group. If one uri in cache group sets a TTL 203 | // then entire group will be evicted. This is a short coming of using 204 | // hashmap as a group. Needs some work here. 205 | if ttl.Seconds() > 0 { 206 | if err := p.PExpire(s.ctx, key, ttl).Err(); err != nil { 207 | return err 208 | } 209 | } 210 | 211 | _, err := p.Exec(s.ctx) 212 | return err 213 | } 214 | 215 | func (s *Store) putWorker() { 216 | var ( 217 | p = s.cn.Pipeline() 218 | count = 0 219 | ticker = time.NewTicker(s.config.AsyncCommitFreq) 220 | ) 221 | defer ticker.Stop() 222 | 223 | for { 224 | select { 225 | case req := <-s.putBuf: 226 | key := s.key(req.namespace, req.group) 227 | if err := p.HMSet(s.ctx, key, map[string]interface{}{ 228 | s.field(keyCtype, req.uri): req.b.ContentType, 229 | s.field(keyEtag, req.uri): req.b.ETag, 230 | s.field(keyCompression, req.uri): req.b.Compression, 231 | s.field(keyBlob, req.uri): req.b.Blob, 232 | }).Err(); err != nil { 233 | // Log error 234 | continue 235 | } 236 | 237 | // Set a TTL for the group. If one uri in cache group sets a TTL 238 | // then entire group will be evicted. This is a shortcoming of using 239 | // hashmap as a group. Needs some work here. 240 | if req.ttl.Seconds() > 0 { 241 | if err := p.PExpire(s.ctx, key, req.ttl).Err(); err != nil { 242 | // Log error 243 | continue 244 | } 245 | } 246 | 247 | if count++; count > s.config.AsyncMaxCommitSize { 248 | if _, err := p.Exec(s.ctx); err != nil { 249 | s.logger.Printf("goredis-store: error committing async writes: %v", err) 250 | } 251 | count = 0 252 | p = s.cn.Pipeline() 253 | } 254 | 255 | case <-ticker.C: 256 | if count > 0 { 257 | if _, err := p.Exec(s.ctx); err != nil { 258 | s.logger.Printf("goredis-store: error committing ticker async writes: %v", err) 259 | } 260 | count = 0 261 | p = s.cn.Pipeline() 262 | } 263 | 264 | case <-s.ctx.Done(): 265 | return 266 | } 267 | } 268 | } 269 | 270 | // Del deletes a single cached URI. 271 | func (s *Store) Del(namespace, group, uri string) error { 272 | return s.cn.HDel(s.ctx, s.key(namespace, group), 273 | s.field(keyCtype, uri), 274 | s.field(keyEtag, uri), 275 | s.field(keyCompression, uri), 276 | s.field(keyBlob, uri)).Err() 277 | } 278 | 279 | // DelGroup deletes a whole group. 280 | func (s *Store) DelGroup(namespace string, groups ...string) error { 281 | p := s.cn.Pipeline() 282 | for _, group := range groups { 283 | if err := p.Del(s.ctx, s.key(namespace, group)).Err(); err != nil { 284 | return err 285 | } 286 | } 287 | 288 | _, err := p.Exec(s.ctx) 289 | return err 290 | } 291 | 292 | func (s *Store) key(namespace, group string) string { 293 | return s.config.Prefix + namespace + sep + group 294 | } 295 | 296 | func (s *Store) field(key string, uri string) string { 297 | return key + "_" + uri 298 | } 299 | 300 | // stringToBytes converts string to byte slice using unsafe. 301 | // Copied from: https://github.com/go-redis/redis/blob/803592d454c49277405303fa6261dc090db542d2/internal/util/unsafe.go 302 | // Context: https://github.com/redis/go-redis/issues/1618 303 | func stringToBytes(s string) []byte { 304 | return *(*[]byte)(unsafe.Pointer( 305 | &struct { 306 | string 307 | Cap int 308 | }{s, len(s)}, 309 | )) 310 | } 311 | -------------------------------------------------------------------------------- /stores/goredis/redis_cluster_test.go: -------------------------------------------------------------------------------- 1 | //go:build clustertest 2 | // +build clustertest 3 | 4 | package goredis 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/redis/go-redis/v9" 15 | "github.com/stretchr/testify/require" 16 | "github.com/testcontainers/testcontainers-go" 17 | "github.com/testcontainers/testcontainers-go/wait" 18 | "github.com/zerodha/fastcache/v4" 19 | ) 20 | 21 | const ( 22 | redisImage = "docker.io/bitnami/redis-cluster:6.0-debian-10" 23 | numNodes = 6 24 | ) 25 | 26 | func setupRedisCluster(t *testing.T) (*redis.ClusterClient, func(), func(int)) { 27 | ctx := context.Background() 28 | 29 | // Create a network 30 | // Incase of a network error, try running the following command: 31 | // docker network rm fc-redis-cluster-network 32 | networkName := "fc-redis-cluster-network" 33 | network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ 34 | NetworkRequest: testcontainers.NetworkRequest{ 35 | Name: networkName, 36 | Driver: "bridge", 37 | }, 38 | }) 39 | require.NoError(t, err) 40 | 41 | // Start Redis nodes 42 | nodes := make([]testcontainers.Container, numNodes) 43 | wg := sync.WaitGroup{} 44 | wg.Add(numNodes) 45 | for i := 0; i < numNodes; i++ { 46 | go func(i int) { 47 | defer wg.Done() 48 | nodeName := fmt.Sprintf("redis-node-%d", i) 49 | req := testcontainers.ContainerRequest{ 50 | Image: redisImage, 51 | ExposedPorts: []string{"6379/tcp"}, 52 | Name: nodeName, 53 | Networks: []string{networkName}, 54 | Env: map[string]string{ 55 | "ALLOW_EMPTY_PASSWORD": "yes", 56 | "REDIS_NODES": "redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5", 57 | }, 58 | WaitingFor: wait.ForLog(".*(?:Background AOF rewrite finished successfully|Synchronization with replica (?:\\d{1,3}\\.){3}\\d{1,3}:\\d+ succeeded).*").AsRegexp(), 59 | } 60 | 61 | node, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 62 | ContainerRequest: req, 63 | Started: true, 64 | }) 65 | require.NoError(t, err) 66 | nodes[i] = node 67 | }(i) 68 | } 69 | 70 | // Initialize the cluster 71 | initReq := testcontainers.ContainerRequest{ 72 | Image: redisImage, 73 | Name: "redis-cluster-init", 74 | Networks: []string{networkName}, 75 | Env: map[string]string{ 76 | "ALLOW_EMPTY_PASSWORD": "yes", 77 | "REDIS_CLUSTER_REPLICAS": "1", 78 | "REDIS_NODES": "redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5", 79 | "REDIS_CLUSTER_CREATOR": "yes", 80 | }, 81 | WaitingFor: wait.ForLog(""), 82 | } 83 | 84 | initContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 85 | ContainerRequest: initReq, 86 | Started: true, 87 | }) 88 | require.NoError(t, err) 89 | 90 | wg.Wait() 91 | log.Println("Redis cluster initialized, and all nodes are up and running.") 92 | 93 | // Get the IP addresses of the nodes 94 | var nodeAddresses []string 95 | for _, node := range nodes { 96 | ip, err := node.ContainerIP(ctx) 97 | require.NoError(t, err) 98 | nodeAddresses = append(nodeAddresses, fmt.Sprintf("%s:6379", ip)) 99 | } 100 | 101 | // Create a cluster client 102 | client := redis.NewClusterClient(&redis.ClusterOptions{ 103 | Addrs: nodeAddresses, 104 | }) 105 | 106 | require.NoError(t, client.Ping(ctx).Err()) 107 | 108 | cleanup := func() { 109 | client.Close() 110 | for _, node := range nodes { 111 | node.Terminate(ctx) 112 | } 113 | initContainer.Terminate(ctx) 114 | network.Remove(ctx) 115 | } 116 | 117 | kill := func(i int) { 118 | nodes[i].Stop(ctx, nil) 119 | } 120 | 121 | return client, cleanup, kill 122 | } 123 | 124 | func TestAsyncWritesToCluster(t *testing.T) { 125 | t.Skip() 126 | redisCluster, cleanup, _ := setupRedisCluster(t) 127 | defer cleanup() 128 | 129 | testPrefix := "TEST:" 130 | testNamespace := "namespace" 131 | testGroup := "group" 132 | testEndpoint := "/test/endpoint" 133 | testItem := fastcache.Item{ 134 | ETag: "etag", 135 | ContentType: "content_type", 136 | Blob: []byte("{}"), 137 | } 138 | 139 | pool := New(Config{ 140 | Prefix: testPrefix, 141 | Async: true, 142 | AsyncMaxCommitSize: 5, 143 | AsyncBufSize: 10, 144 | AsyncCommitFreq: 100 * time.Millisecond, 145 | }, redisCluster) 146 | 147 | // Perform async writes 148 | for i := 0; i < 20; i++ { 149 | err := pool.Put(testNamespace, testGroup, fmt.Sprintf("%s%d", testEndpoint, i), testItem, time.Second*3) 150 | require.NoError(t, err) 151 | } 152 | 153 | // Wait for async writes to complete 154 | time.Sleep(200 * time.Millisecond) 155 | 156 | // Verify the writes 157 | for i := 0; i < 20; i++ { 158 | item, err := pool.Get(testNamespace, testGroup, fmt.Sprintf("%s%d", testEndpoint, i)) 159 | require.NoError(t, err) 160 | require.Equal(t, testItem, item) 161 | } 162 | 163 | // Test deletion 164 | err := pool.DelGroup(testNamespace, testGroup) 165 | require.NoError(t, err) 166 | 167 | // Verify deletion 168 | _, err = pool.Get(testNamespace, testGroup, fmt.Sprintf("%s%d", testEndpoint, 0)) 169 | require.Error(t, err) 170 | } 171 | 172 | func TestAsyncWritesWithNodeFailure(t *testing.T) { 173 | redisCluster, cleanup, kill := setupRedisCluster(t) 174 | defer cleanup() 175 | 176 | testPrefix := "TEST:" 177 | testNamespace := "namespace" 178 | testGroup := "group" 179 | testEndpoint := "/test/endpoint" 180 | testItem := fastcache.Item{ 181 | ETag: "etag", 182 | ContentType: "content_type", 183 | Blob: []byte("{}"), 184 | } 185 | 186 | pool := New(Config{ 187 | Prefix: testPrefix, 188 | Async: true, 189 | AsyncMaxCommitSize: 50, 190 | AsyncBufSize: 50, 191 | AsyncCommitFreq: 100 * time.Millisecond, 192 | }, redisCluster) 193 | 194 | // Start async writes 195 | for i := 0; i < 10; i++ { 196 | err := pool.Put(testNamespace, testGroup, fmt.Sprintf("%s%d:{%d}", testEndpoint, i, i%3), testItem, time.Second*3) 197 | require.NoError(t, err) 198 | } 199 | 200 | // Lets complete the async writes 201 | time.Sleep(200 * time.Millisecond) 202 | 203 | // Simulate node failure by removing a node from the cluster 204 | kill(1) 205 | kill(4) 206 | 207 | time.Sleep(5000 * time.Millisecond) 208 | 209 | // Continue with more writes 210 | for i := 10; i < 20; i++ { 211 | err := pool.Put(testNamespace, testGroup, fmt.Sprintf("%s%d:{%d}", testEndpoint, i, i%3), testItem, time.Second*3) 212 | require.NoError(t, err) 213 | } 214 | 215 | // Wait for async writes to complete 216 | time.Sleep(200 * time.Millisecond) 217 | 218 | // Verify the writes 219 | successCount := 0 220 | for i := 0; i < 20; i++ { 221 | item, err := pool.Get(testNamespace, testGroup, fmt.Sprintf("%s%d:{%d}", testEndpoint, i, i%3)) 222 | if err == nil && item.ETag == testItem.ETag && item.ContentType == testItem.ContentType { 223 | successCount++ 224 | } 225 | } 226 | 227 | // We expect some writes to succeed, but not all due to the node failure 228 | require.True(t, successCount > 0 && successCount < 20, "Expected some successful writes, but not all. Got %d successful writes", successCount) 229 | 230 | // Test deletion 231 | err := pool.DelGroup(testNamespace, testGroup) 232 | require.NoError(t, err) 233 | 234 | // Verify deletion 235 | _, err = pool.Get(testNamespace, testGroup, fmt.Sprintf("%s%d", testEndpoint, 0)) 236 | require.Error(t, err) 237 | } 238 | -------------------------------------------------------------------------------- /stores/goredis/redis_test.go: -------------------------------------------------------------------------------- 1 | package goredis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/alicebob/miniredis/v2" 10 | "github.com/redis/go-redis/v9" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/zerodha/fastcache/v4" 13 | ) 14 | 15 | func newTestRedis(t *testing.T) *redis.Client { 16 | mr, err := miniredis.Run() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | client := redis.NewClient(&redis.Options{ 22 | Addr: mr.Addr(), 23 | }) 24 | 25 | assert.Nil(t, client.Ping(context.TODO()).Err()) 26 | 27 | return client 28 | } 29 | 30 | func TestNew(t *testing.T) { 31 | redisClient := newTestRedis(t) 32 | 33 | testPrefix := "TEST:" 34 | testNamespace := "namespace" 35 | testGroup := "group" 36 | testEndpoint := "/test/endpoint" 37 | testItem := fastcache.Item{ 38 | ETag: "etag", 39 | ContentType: "content_type", 40 | Blob: []byte("{}"), 41 | } 42 | for _, async := range []bool{true, false} { 43 | t.Run(fmt.Sprintf("async=%v", async), func(t *testing.T) { 44 | pool := New(Config{ 45 | Prefix: testPrefix, 46 | Async: async, 47 | AsyncMaxCommitSize: 5, 48 | AsyncBufSize: 10, 49 | AsyncCommitFreq: 100 * time.Millisecond, 50 | }, redisClient) 51 | 52 | // Check empty get, should return proper error and not panic. 53 | _, err := pool.Get(testNamespace, testGroup, testEndpoint) 54 | assert.NotNil(t, err) 55 | 56 | // Place something in cache, 57 | err = pool.Put(testNamespace, testGroup, testEndpoint, testItem, time.Second*3) 58 | assert.Nil(t, err) 59 | 60 | if async { 61 | time.Sleep(200 * time.Millisecond) 62 | } 63 | 64 | // Retrieve cache 65 | item, err := pool.Get(testNamespace, testGroup, testEndpoint) 66 | assert.Nil(t, err) 67 | assert.Equal(t, testItem, item) 68 | 69 | // Invalidate 70 | err = pool.Del(testNamespace, testGroup, testEndpoint) 71 | assert.Nil(t, err) 72 | 73 | // Check empty get, should return proper error and not panic. 74 | _, err = pool.Get(testNamespace, testGroup, testEndpoint) 75 | assert.NotNil(t, err) 76 | 77 | // Invalidate 78 | err = pool.DelGroup(testNamespace, testGroup) 79 | assert.Nil(t, err) 80 | 81 | // Check empty get, should return proper error and not panic. 82 | _, err = pool.Get(testNamespace, testGroup, testEndpoint) 83 | assert.NotNil(t, err) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /stores/redis/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerodha/fastcache/stores/redis 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gomodule/redigo v2.0.0+incompatible 7 | github.com/zerodha/fastcache/v4 v4.0.0 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.4 // indirect 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/fasthttp/router v1.4.5 // indirect 14 | github.com/klauspost/compress v1.15.0 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect 17 | github.com/stretchr/testify v1.6.1 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | github.com/valyala/fasthttp v1.34.0 // indirect 20 | github.com/zerodha/fastglue v1.7.1 // indirect 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /stores/redis/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 2 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 3 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fasthttp/router v1.4.5 h1:YZonsKCssEwEi3veDMhL6okIx550qegAiuXAK8NnM3Y= 8 | github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXhaTRDG9Y= 9 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 11 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 12 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 14 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 15 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= 16 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI= 20 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 24 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 26 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 27 | github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= 28 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 29 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 30 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 31 | github.com/zerodha/fastcache/v4 v4.0.0 h1:cxJ2AwLAxxvWiarB2/P+PEWAU6/ZfNY3bkBkWXzQKbs= 32 | github.com/zerodha/fastcache/v4 v4.0.0/go.mod h1:jfFLkiuyMIO8u7KlojdeLGaVyPuVXES0PRGxS7byr6s= 33 | github.com/zerodha/fastglue v1.7.1 h1:YbKiSSEYzDmVDM29KCeXMHuh+48TcEHsgYCIGjoUcbU= 34 | github.com/zerodha/fastglue v1.7.1/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= 35 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 36 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 37 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 38 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 39 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 40 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 41 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 48 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 49 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 50 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 51 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 52 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 56 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | -------------------------------------------------------------------------------- /stores/redis/pool.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gomodule/redigo/redis" 7 | ) 8 | 9 | // NewPool returns a Redigo cachepool. 10 | func NewPool(address string, password string, maxActiv int, maxIdle int, timeout time.Duration) *redis.Pool { 11 | return &redis.Pool{ 12 | Wait: true, 13 | MaxActive: maxActiv, 14 | MaxIdle: maxIdle, 15 | Dial: func() (redis.Conn, error) { 16 | c, err := redis.Dial( 17 | "tcp", 18 | address, 19 | redis.DialPassword(password), 20 | redis.DialConnectTimeout(timeout), 21 | redis.DialReadTimeout(timeout), 22 | redis.DialWriteTimeout(timeout), 23 | ) 24 | 25 | return c, err 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stores/redis/redis.go: -------------------------------------------------------------------------------- 1 | // Package redis implements a Redis cache storage backend for fastcache. 2 | // The internal structure looks like this where 3 | // XX1234 = namespace, marketwach = group 4 | // ``` 5 | // 6 | // CACHE:XX1234:marketwatch { 7 | // "/user/marketwatch_ctype" -> []byte 8 | // "/user/marketwatch_etag" -> []byte 9 | // "/user/marketwatch_blob" -> []byte 10 | // "/user/marketwatch/123_ctype" -> []byte 11 | // "/user/marketwatch/123_etag" -> []byte 12 | // "/user/marketwatch/123_blob" -> []byte 13 | // } 14 | // 15 | // ``` 16 | package redis 17 | 18 | import ( 19 | "time" 20 | 21 | "github.com/gomodule/redigo/redis" 22 | "github.com/zerodha/fastcache/v4" 23 | ) 24 | 25 | const ( 26 | // Store keys. 27 | keyEtag = "_etag" 28 | keyCtype = "_ctype" 29 | keyCompression = "_comp" 30 | keyBlob = "_blob" 31 | 32 | sep = ":" 33 | ) 34 | 35 | // Store is a Redis cache store implementation for fastcache. 36 | type Store struct { 37 | prefix string 38 | pool *redis.Pool 39 | } 40 | 41 | // New creates a new Redis instance. prefix is the prefix to apply to all 42 | // cache keys. 43 | func New(prefix string, pool *redis.Pool) *Store { 44 | return &Store{ 45 | prefix: prefix, 46 | pool: pool, 47 | } 48 | } 49 | 50 | // Get gets the fastcache.Item for a single cached URI. 51 | func (s *Store) Get(namespace, group, uri string) (fastcache.Item, error) { 52 | cn := s.pool.Get() 53 | defer cn.Close() 54 | 55 | var out fastcache.Item 56 | // Get content_type, etag, blob in that order. 57 | resp, err := redis.ByteSlices(cn.Do("HMGET", s.key(namespace, group), s.field(keyCtype, uri), s.field(keyEtag, uri), s.field(keyCompression, uri), s.field(keyBlob, uri))) 58 | if err != nil { 59 | return out, err 60 | } 61 | 62 | out = fastcache.Item{ 63 | ContentType: string(resp[0]), 64 | ETag: string(resp[1]), 65 | Compression: string(resp[2]), 66 | Blob: resp[3], 67 | } 68 | return out, err 69 | } 70 | 71 | // Put sets a value to given session but stored only on commit 72 | func (s *Store) Put(namespace, group, uri string, b fastcache.Item, ttl time.Duration) error { 73 | cn := s.pool.Get() 74 | defer cn.Close() 75 | 76 | key := s.key(namespace, group) 77 | if err := cn.Send("HMSET", key, 78 | s.field(keyCtype, uri), b.ContentType, 79 | s.field(keyEtag, uri), b.ETag, 80 | s.field(keyCompression, uri), b.Compression, 81 | s.field(keyBlob, uri), b.Blob); err != nil { 82 | return err 83 | } 84 | 85 | // Set a TTL for the group. If one uri in cache group sets a TTL 86 | // then entire group will be evicted. This is a short coming of using 87 | // hashmap as a group. Needs some work here. 88 | if ttl.Seconds() > 0 { 89 | exp := ttl.Nanoseconds() / int64(time.Millisecond) 90 | if err := cn.Send("PEXPIRE", key, exp); err != nil { 91 | return err 92 | } 93 | } 94 | return cn.Flush() 95 | } 96 | 97 | // Del deletes a single cached URI. 98 | func (s *Store) Del(namespace, group, uri string) error { 99 | cn := s.pool.Get() 100 | defer cn.Close() 101 | 102 | if err := cn.Send("HDEL", s.key(namespace, group), s.field(keyCtype, uri), s.field(keyEtag, uri), s.field(keyCompression, uri), s.field(keyBlob, uri)); err != nil { 103 | return err 104 | } 105 | 106 | return cn.Flush() 107 | } 108 | 109 | // DelGroup deletes a whole group. 110 | func (s *Store) DelGroup(namespace string, groups ...string) error { 111 | cn := s.pool.Get() 112 | defer cn.Close() 113 | 114 | for _, group := range groups { 115 | if err := cn.Send("DEL", s.key(namespace, group)); err != nil { 116 | return err 117 | } 118 | } 119 | return cn.Flush() 120 | } 121 | 122 | func (s *Store) key(namespace, group string) string { 123 | return s.prefix + namespace + sep + group 124 | } 125 | 126 | func (s *Store) field(key string, uri string) string { 127 | return key + "_" + uri 128 | } 129 | -------------------------------------------------------------------------------- /tests/fastcache_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "testing" 13 | "time" 14 | 15 | "github.com/alicebob/miniredis" 16 | redis "github.com/redis/go-redis/v9" 17 | "github.com/valyala/fasthttp" 18 | cachestore "github.com/zerodha/fastcache/stores/goredis/v9" 19 | "github.com/zerodha/fastcache/v4" 20 | "github.com/zerodha/fastglue" 21 | ) 22 | 23 | const ( 24 | namespaceKey = "req" 25 | group = "test" 26 | ) 27 | 28 | var ( 29 | srv = fastglue.NewGlue() 30 | srvAddr = dummyServAddr() 31 | srvRoot = "http://127.0.0.1" + srvAddr 32 | 33 | content = []byte("this is the reasonbly long test content that may be compressed") 34 | ) 35 | 36 | // dummyServeAddr returns a random port address. 37 | func dummyServAddr() string { 38 | // Dynamically allocate an available port 39 | listener, err := net.Listen("tcp", ":0") 40 | if err != nil { 41 | panic(err) 42 | } 43 | defer listener.Close() 44 | 45 | // Get the actual port that was allocated 46 | port := listener.Addr().(*net.TCPAddr).Port 47 | 48 | return fmt.Sprintf(":%d", port) 49 | } 50 | 51 | func init() { 52 | // Setup fastcache. 53 | rd, err := miniredis.Run() 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | var ( 59 | cfgDefault = &fastcache.Options{ 60 | NamespaceKey: namespaceKey, 61 | ETag: true, 62 | TTL: time.Second * 5, 63 | Logger: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile), 64 | Compression: fastcache.CompressionsOptions{ 65 | Enabled: true, 66 | MinLength: 10, 67 | }, 68 | } 69 | 70 | cfgCompressed = &fastcache.Options{ 71 | NamespaceKey: namespaceKey, 72 | ETag: true, 73 | TTL: time.Second * 5, 74 | Logger: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile), 75 | Compression: fastcache.CompressionsOptions{ 76 | Enabled: true, 77 | MinLength: 10, 78 | RespectHeaders: true, 79 | }, 80 | } 81 | 82 | noBlob = &fastcache.Options{ 83 | NamespaceKey: namespaceKey, 84 | ETag: true, 85 | TTL: time.Second * 60, 86 | NoBlob: true, 87 | Logger: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile), 88 | } 89 | 90 | fc = fastcache.New(cachestore.New(cachestore.Config{ 91 | Prefix: "CACHE:", 92 | Async: false, 93 | }, redis.NewClient(&redis.Options{ 94 | Addr: rd.Addr(), 95 | }))) 96 | ) 97 | 98 | // Handlers. 99 | srv.Before(func(r *fastglue.Request) *fastglue.Request { 100 | r.RequestCtx.SetUserValue(namespaceKey, "test") 101 | return r 102 | }) 103 | 104 | srv.GET("/cached", fc.Cached(func(r *fastglue.Request) error { 105 | return r.SendBytes(200, "text/plain", content) 106 | }, cfgDefault, group)) 107 | 108 | srv.GET("/no-store", fc.Cached(func(r *fastglue.Request) error { 109 | r.RequestCtx.Response.Header.Set("Cache-Control", "no-store") 110 | return r.SendBytes(200, "text/plain", content) 111 | }, cfgDefault, group)) 112 | 113 | srv.GET("/no-blob", fc.Cached(func(r *fastglue.Request) error { 114 | return r.SendBytes(200, "text/plain", content) 115 | }, noBlob, group)) 116 | 117 | srv.GET("/compressed", fc.Cached(func(r *fastglue.Request) error { 118 | return r.SendBytes(200, "text/plain", content) 119 | }, cfgCompressed, group)) 120 | 121 | srv.GET("/clear-group", fc.ClearGroup(func(r *fastglue.Request) error { 122 | return r.SendBytes(200, "text/plain", content) 123 | }, cfgDefault, group)) 124 | 125 | // Start the server 126 | go func() { 127 | s := &fasthttp.Server{ 128 | Name: "test", 129 | ReadTimeout: 5 * time.Second, 130 | WriteTimeout: 5 * time.Second, 131 | } 132 | if err := srv.ListenAndServe(srvAddr, "", s); err != nil { 133 | log.Fatalf("error starting HTTP server: %s", err) 134 | } 135 | }() 136 | 137 | time.Sleep(time.Millisecond * 100) 138 | } 139 | 140 | func getReq(url, etag string, gzipped bool, t *testing.T) (*http.Response, []byte) { 141 | client := http.Client{} 142 | req, err := http.NewRequest("GET", url, nil) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | if etag != "" { 148 | req.Header = http.Header{ 149 | "If-None-Match": []string{etag}, 150 | } 151 | } 152 | 153 | if gzipped { 154 | req.Header.Set("Accept-Encoding", "gzip") 155 | } 156 | 157 | resp, err := client.Do(req) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | 162 | b, err := io.ReadAll(resp.Body) 163 | if err != nil { 164 | t.Fatal(b) 165 | } 166 | 167 | return resp, b 168 | } 169 | 170 | func TestCache(t *testing.T) { 171 | // First request should be 200. 172 | r, b := getReq(srvRoot+"/cached", "", false, t) 173 | if r.StatusCode != 200 { 174 | t.Fatalf("expected 200 but got %v", r.StatusCode) 175 | } 176 | if !bytes.Equal(b, content) { 177 | t.Fatalf("expected 'ok' in body but got %v", b) 178 | } 179 | 180 | // Second should be 304. 181 | r, b = getReq(srvRoot+"/cached", r.Header.Get("Etag"), false, t) 182 | if r.StatusCode != 304 { 183 | t.Fatalf("expected 304 but got '%v'", r.StatusCode) 184 | } 185 | if !bytes.Equal(b, []byte("")) { 186 | t.Fatalf("expected empty cached body but got '%v'", b) 187 | } 188 | 189 | // Wrong etag. 190 | r, b = getReq(srvRoot+"/cached", "wrong", false, t) 191 | if r.StatusCode != 200 { 192 | t.Fatalf("expected 200 but got '%v'", r.StatusCode) 193 | } 194 | 195 | // Clear cache. 196 | r, b = getReq(srvRoot+"/clear-group", "", false, t) 197 | if r.StatusCode != 200 { 198 | t.Fatalf("expected 200 but got %v", r.StatusCode) 199 | } 200 | r, b = getReq(srvRoot+"/cached", r.Header.Get("Etag"), false, t) 201 | if r.StatusCode != 200 { 202 | t.Fatalf("expected 200 but got '%v'", r.StatusCode) 203 | } 204 | 205 | // Compressed blob. 206 | r, b = getReq(srvRoot+"/compressed", "", false, t) 207 | if r.StatusCode != 200 { 208 | t.Fatalf("expected 200 but got '%v'", r.StatusCode) 209 | } 210 | // Uncompressed output. 211 | if !bytes.Equal(b, content) { 212 | t.Fatalf("expected test content in body but got %v", b) 213 | } 214 | 215 | // Compressed output. 216 | r, b = getReq(srvRoot+"/compressed", r.Header.Get("Etag"), true, t) 217 | if r.StatusCode != 304 { 218 | t.Fatalf("expected 304 but got '%v'", r.StatusCode) 219 | } 220 | 221 | r, b = getReq(srvRoot+"/compressed", "", true, t) 222 | if r.StatusCode != 200 { 223 | t.Fatalf("expected 200 but got '%v'", r.StatusCode) 224 | } 225 | 226 | decomp, err := decompressGzip(b) 227 | if err != nil { 228 | t.Fatalf("error decompressing gzip: %v", err) 229 | } 230 | 231 | if !bytes.Equal(decomp, content) { 232 | t.Fatalf("expected test content in body but got %v", b) 233 | } 234 | } 235 | 236 | func TestNoCache(t *testing.T) { 237 | // All requests should return 200. 238 | for n := 0; n < 3; n++ { 239 | r, b := getReq(srvRoot+"/no-store", "", false, t) 240 | if r.StatusCode != 200 { 241 | t.Fatalf("expected 200 but got %v", r.StatusCode) 242 | } 243 | if r.Header.Get("Etag") != "" { 244 | t.Fatal("there should be no etag for no-store response") 245 | } 246 | if !bytes.Equal(b, content) { 247 | t.Fatalf("expected 'ok' in body but got %v", b) 248 | } 249 | } 250 | } 251 | 252 | func TestNoBlob(t *testing.T) { 253 | // All requests should return 200. 254 | eTag := "" 255 | for n := 0; n < 3; n++ { 256 | r, _ := getReq(srvRoot+"/no-blob", eTag, false, t) 257 | if n == 0 { 258 | eTag = r.Header.Get("Etag") 259 | if r.StatusCode != 200 { 260 | t.Fatalf("expected 200 but got %v", r.StatusCode) 261 | } 262 | continue 263 | } 264 | 265 | if r.StatusCode != 304 { 266 | t.Fatalf("expected 304 but got %v", r.StatusCode) 267 | } 268 | } 269 | } 270 | 271 | func decompressGzip(b []byte) ([]byte, error) { 272 | r, err := gzip.NewReader(bytes.NewReader(b)) 273 | if err != nil { 274 | return nil, err 275 | } 276 | defer r.Close() 277 | 278 | return io.ReadAll(r) 279 | } 280 | -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerodha/fastcache/tests 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alicebob/miniredis v2.5.0+incompatible 7 | github.com/redis/go-redis/v9 v9.5.1 8 | github.com/valyala/fasthttp v1.52.0 9 | github.com/zerodha/fastcache/stores/goredis/v9 v9.0.0 10 | github.com/zerodha/fastcache/v4 v4.1.0 11 | github.com/zerodha/fastglue v1.8.0 12 | ) 13 | 14 | require ( 15 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect 16 | github.com/andybalholm/brotli v1.1.0 // indirect 17 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 20 | github.com/fasthttp/router v1.4.5 // indirect 21 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 22 | github.com/klauspost/compress v1.17.6 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect 25 | github.com/stretchr/testify v1.9.0 // indirect 26 | github.com/valyala/bytebufferpool v1.0.0 // indirect 27 | github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /tests/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U= 2 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 3 | github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= 4 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= 5 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 6 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 7 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 8 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 9 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 10 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 11 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 12 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 14 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 15 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 21 | github.com/fasthttp/router v1.4.5 h1:YZonsKCssEwEi3veDMhL6okIx550qegAiuXAK8NnM3Y= 22 | github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXhaTRDG9Y= 23 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 24 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 25 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 26 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 28 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 29 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 30 | github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= 31 | github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= 35 | github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 36 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI= 37 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 41 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 42 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 43 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 44 | github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= 45 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 46 | github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= 47 | github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= 48 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 49 | github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= 50 | github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= 51 | github.com/zerodha/fastcache/stores/goredis/v9 v9.0.0 h1:ivJqPGW6zELQ+XeFO64pUu6HjU5/12VG9S8t5qSJQoY= 52 | github.com/zerodha/fastcache/stores/goredis/v9 v9.0.0/go.mod h1:KPyw7sIXu+bjEQ4x1I784SftawpcnIAiqZQpK8TXBjY= 53 | github.com/zerodha/fastcache/v4 v4.1.0 h1:6QsEw+orXoras88mCrDlys3OZL7FU6oiw/tM6q4gRbc= 54 | github.com/zerodha/fastcache/v4 v4.1.0/go.mod h1:jfFLkiuyMIO8u7KlojdeLGaVyPuVXES0PRGxS7byr6s= 55 | github.com/zerodha/fastglue v1.8.0 h1:yCfb8YwZLoFrzHiojRcie19olLDT48vjuinVn1Ge5Uc= 56 | github.com/zerodha/fastglue v1.8.0/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= 57 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 58 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 59 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 60 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 61 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 62 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 63 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 71 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 72 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 74 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 75 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | --------------------------------------------------------------------------------