Expiring cache

This commit is contained in:
Travis Reeder
2017-07-20 08:44:56 -07:00
committed by Derek Schultz
parent 2be96689d4
commit c3630eaa41
18 changed files with 3401 additions and 167 deletions

View File

@@ -20,10 +20,6 @@ var (
code: http.StatusGatewayTimeout, code: http.StatusGatewayTimeout,
error: errors.New("Timed out"), error: errors.New("Timed out"),
} }
ErrRunnerRouteNotFound = err{
code: http.StatusNotFound,
error: errors.New("Route not found on that application"),
}
ErrAppsValidationMissingName = err{ ErrAppsValidationMissingName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing app name"), error: errors.New("Missing app name"),

View File

@@ -1,88 +0,0 @@
// Package routecache is meant to assist in resolving the most used routes at
// an application. Implemented as a LRU, it returns always its full context for
// iteration at the router handler.
package routecache
// based on groupcache's LRU
import (
"container/list"
"gitlab-odx.oracle.com/odx/functions/api/models"
)
// Cache holds an internal linkedlist for hotness management. It is not safe
// for concurrent use, must be guarded externally.
type Cache struct {
MaxEntries int
ll *list.List
cache map[string]*list.Element
}
// New returns a route cache.
func New(maxentries int) *Cache {
return &Cache{
MaxEntries: maxentries,
ll: list.New(),
cache: make(map[string]*list.Element),
}
}
// Refresh updates internal linkedlist either adding a new route to the front,
// or moving it to the front when used. It will discard seldom used routes.
func (c *Cache) Refresh(route *models.Route) {
if c.cache == nil {
return
}
if ee, ok := c.cache[route.AppName+route.Path]; ok {
c.ll.MoveToFront(ee)
ee.Value = route
return
}
ele := c.ll.PushFront(route)
c.cache[route.AppName+route.Path] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.removeOldest()
}
}
// Get looks up a path's route from the cache.
func (c *Cache) Get(appname, path string) (route *models.Route, ok bool) {
if c.cache == nil {
return
}
if ele, hit := c.cache[appname+path]; hit {
c.ll.MoveToFront(ele)
return ele.Value.(*models.Route), true
}
return
}
// Delete removes the element for the given appname and path from the cache.
func (c *Cache) Delete(appname, path string) {
if ele, hit := c.cache[appname+path]; hit {
c.removeElement(ele)
}
}
func (c *Cache) removeOldest() {
if c.cache == nil {
return
}
if ele := c.ll.Back(); ele != nil {
c.removeElement(ele)
}
}
func (c *Cache) removeElement(e *list.Element) {
c.ll.Remove(e)
kv := e.Value.(*models.Route)
delete(c.cache, kv.AppName+kv.Path)
}
func (c *Cache) Len() int {
return len(c.cache)
}

View File

@@ -47,7 +47,7 @@ func (s *Server) handleRouteCreateOrUpdate(c *gin.Context) {
return return
} }
s.cacheRefresh(resp.Route) s.cachedelete(resp.Route.AppName, resp.Route.Path)
c.JSON(http.StatusOK, resp) c.JSON(http.StatusOK, resp)
} }

View File

@@ -14,6 +14,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
cache "github.com/patrickmn/go-cache"
"gitlab-odx.oracle.com/odx/functions/api" "gitlab-odx.oracle.com/odx/functions/api"
"gitlab-odx.oracle.com/odx/functions/api/id" "gitlab-odx.oracle.com/odx/functions/api/id"
"gitlab-odx.oracle.com/odx/functions/api/models" "gitlab-odx.oracle.com/odx/functions/api/models"
@@ -44,7 +45,7 @@ func (s *Server) handleSpecial(c *gin.Context) {
c.Request = r c.Request = r
c.Set(api.AppName, r.Context().Value(api.AppName).(string)) c.Set(api.AppName, r.Context().Value(api.AppName).(string))
if c.MustGet(api.AppName).(string) == "" { if c.MustGet(api.AppName).(string) == "" {
handleErrorResponse(c, models.ErrRunnerRouteNotFound) handleErrorResponse(c, models.ErrRoutesNotFound)
return return
} }
@@ -111,40 +112,45 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) {
} }
log.WithFields(logrus.Fields{"app": appName, "path": path}).Debug("Finding route on datastore") log.WithFields(logrus.Fields{"app": appName, "path": path}).Debug("Finding route on datastore")
routes, err := s.loadroutes(ctx, models.RouteFilter{AppName: appName, Path: path}) route, err := s.loadroute(ctx, appName, path)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleErrorResponse(c, err)
return return
} }
if len(routes) == 0 { if route == nil {
handleErrorResponse(c, models.ErrRunnerRouteNotFound) handleErrorResponse(c, models.ErrRoutesNotFound)
return return
} }
log.WithField("routes", len(routes)).Debug("Got routes from datastore")
route := routes[0]
log = log.WithFields(logrus.Fields{"app": appName, "path": route.Path, "image": route.Image}) log = log.WithFields(logrus.Fields{"app": appName, "path": route.Path, "image": route.Image})
log.Debug("Got route from datastore")
if s.serve(ctx, c, appName, route, app, path, reqID, payload, enqueue) { if s.serve(ctx, c, appName, route, app, path, reqID, payload, enqueue) {
s.FireAfterDispatch(ctx, reqRoute) s.FireAfterDispatch(ctx, reqRoute)
return return
} }
handleErrorResponse(c, models.ErrRunnerRouteNotFound) handleErrorResponse(c, models.ErrRoutesNotFound)
} }
func (s *Server) loadroutes(ctx context.Context, filter models.RouteFilter) ([]*models.Route, error) { func (s *Server) loadroute(ctx context.Context, appName, path string) (*models.Route, error) {
if route, ok := s.cacheget(filter.AppName, filter.Path); ok { if route, ok := s.cacheget(appName, path); ok {
return []*models.Route{route}, nil return route, nil
} }
key := routeCacheKey(appName, path)
resp, err := s.singleflight.do( resp, err := s.singleflight.do(
filter, key,
func() (interface{}, error) { func() (interface{}, error) {
return s.Datastore.GetRoutesByApp(ctx, filter.AppName, &filter) return s.Datastore.GetRoute(ctx, appName, path)
}, },
) )
return resp.([]*models.Route), err if err != nil {
return nil, err
}
route := resp.(*models.Route)
s.routeCache.Set(key, route, cache.DefaultExpiration)
return route, nil
} }
// TODO: Should remove *gin.Context from these functions, should use only context.Context // TODO: Should remove *gin.Context from these functions, should use only context.Context

View File

@@ -7,13 +7,14 @@ import (
"net/http" "net/http"
"sync" "sync"
"testing" "testing"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
cache "github.com/patrickmn/go-cache"
"gitlab-odx.oracle.com/odx/functions/api/datastore" "gitlab-odx.oracle.com/odx/functions/api/datastore"
"gitlab-odx.oracle.com/odx/functions/api/models" "gitlab-odx.oracle.com/odx/functions/api/models"
"gitlab-odx.oracle.com/odx/functions/api/mqs" "gitlab-odx.oracle.com/odx/functions/api/mqs"
"gitlab-odx.oracle.com/odx/functions/api/runner" "gitlab-odx.oracle.com/odx/functions/api/runner"
"gitlab-odx.oracle.com/odx/functions/api/server/internal/routecache"
) )
func testRouterAsync(ds models.Datastore, mq models.MessageQueue, rnr *runner.Runner, enqueue models.Enqueue) *gin.Engine { func testRouterAsync(ds models.Datastore, mq models.MessageQueue, rnr *runner.Runner, enqueue models.Enqueue) *gin.Engine {
@@ -25,7 +26,7 @@ func testRouterAsync(ds models.Datastore, mq models.MessageQueue, rnr *runner.Ru
Datastore: ds, Datastore: ds,
MQ: mq, MQ: mq,
Enqueue: enqueue, Enqueue: enqueue,
hotroutes: routecache.New(10), routeCache: cache.New(60*time.Second, 5*time.Minute),
} }
r := s.Router r := s.Router

View File

@@ -45,7 +45,7 @@ func TestRouteRunnerGet(t *testing.T) {
}{ }{
{"/route", "", http.StatusNotFound, nil}, {"/route", "", http.StatusNotFound, nil},
{"/r/app/route", "", http.StatusNotFound, models.ErrAppsNotFound}, {"/r/app/route", "", http.StatusNotFound, models.ErrAppsNotFound},
{"/r/myapp/route", "", http.StatusNotFound, models.ErrRunnerRouteNotFound}, {"/r/myapp/route", "", http.StatusNotFound, models.ErrRoutesNotFound},
} { } {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil) _, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
@@ -60,8 +60,8 @@ func TestRouteRunnerGet(t *testing.T) {
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String()) t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`", t.Errorf("Test %d: Expected error message to have `%s`, but got `%s`",
i, test.expectedError.Error()) i, test.expectedError.Error(), resp.Error.Message)
} }
} }
} }
@@ -89,7 +89,7 @@ func TestRouteRunnerPost(t *testing.T) {
}{ }{
{"/route", `{ "payload": "" }`, http.StatusNotFound, nil}, {"/route", `{ "payload": "" }`, http.StatusNotFound, nil},
{"/r/app/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrAppsNotFound}, {"/r/app/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrAppsNotFound},
{"/r/myapp/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrRunnerRouteNotFound}, {"/r/myapp/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrRoutesNotFound},
} { } {
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, srv.Router, "POST", test.path, body) _, rec := routerRequest(t, srv.Router, "POST", test.path, body)

View File

@@ -10,11 +10,12 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"sync" "time"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/ccirello/supervisor" "github.com/ccirello/supervisor"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitlab-odx.oracle.com/odx/functions/api" "gitlab-odx.oracle.com/odx/functions/api"
"gitlab-odx.oracle.com/odx/functions/api/datastore" "gitlab-odx.oracle.com/odx/functions/api/datastore"
@@ -24,7 +25,6 @@ import (
"gitlab-odx.oracle.com/odx/functions/api/mqs" "gitlab-odx.oracle.com/odx/functions/api/mqs"
"gitlab-odx.oracle.com/odx/functions/api/runner" "gitlab-odx.oracle.com/odx/functions/api/runner"
"gitlab-odx.oracle.com/odx/functions/api/runner/common" "gitlab-odx.oracle.com/odx/functions/api/runner/common"
"gitlab-odx.oracle.com/odx/functions/api/server/internal/routecache"
) )
const ( const (
@@ -51,8 +51,7 @@ type Server struct {
middlewares []Middleware middlewares []Middleware
runnerListeners []RunnerListener runnerListeners []RunnerListener
mu sync.Mutex // protects hotroutes routeCache *cache.Cache
hotroutes *routecache.Cache
singleflight singleflight // singleflight assists Datastore singleflight singleflight // singleflight assists Datastore
} }
@@ -99,7 +98,7 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, logDB
Router: gin.New(), Router: gin.New(),
Datastore: ds, Datastore: ds,
MQ: mq, MQ: mq,
hotroutes: routecache.New(cacheSize), routeCache: cache.New(5*time.Second, 5*time.Minute),
LogDB: logDB, LogDB: logDB,
Enqueue: DefaultEnqueue, Enqueue: DefaultEnqueue,
apiURL: apiURL, apiURL: apiURL,
@@ -110,6 +109,9 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, logDB
s.bindHandlers(ctx) s.bindHandlers(ctx)
for _, opt := range opts { for _, opt := range opts {
if opt == nil {
continue
}
opt(s) opt(s)
} }
return s return s
@@ -171,26 +173,19 @@ func DefaultEnqueue(ctx context.Context, mq models.MessageQueue, task *models.Ta
return mq.Push(ctx, task) return mq.Push(ctx, task)
} }
func routeCacheKey(appname, path string) string {
return fmt.Sprintf("%s_%s", appname, path)
}
func (s *Server) cacheget(appname, path string) (*models.Route, bool) { func (s *Server) cacheget(appname, path string) (*models.Route, bool) {
s.mu.Lock() route, ok := s.routeCache.Get(routeCacheKey(appname, path))
defer s.mu.Unlock()
route, ok := s.hotroutes.Get(appname, path)
if !ok { if !ok {
return nil, false return nil, false
} }
return route, ok return route.(*models.Route), ok
}
func (s *Server) cacheRefresh(route *models.Route) {
s.mu.Lock()
defer s.mu.Unlock()
s.hotroutes.Refresh(route)
} }
func (s *Server) cachedelete(appname, path string) { func (s *Server) cachedelete(appname, path string) {
s.mu.Lock() s.routeCache.Delete(routeCacheKey(appname, path))
defer s.mu.Unlock()
s.hotroutes.Delete(appname, path)
} }
func (s *Server) handleRunnerRequest(c *gin.Context) { func (s *Server) handleRunnerRequest(c *gin.Context) {

View File

@@ -10,13 +10,14 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"testing" "testing"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
cache "github.com/patrickmn/go-cache"
"gitlab-odx.oracle.com/odx/functions/api/datastore" "gitlab-odx.oracle.com/odx/functions/api/datastore"
"gitlab-odx.oracle.com/odx/functions/api/models" "gitlab-odx.oracle.com/odx/functions/api/models"
"gitlab-odx.oracle.com/odx/functions/api/mqs" "gitlab-odx.oracle.com/odx/functions/api/mqs"
"gitlab-odx.oracle.com/odx/functions/api/runner" "gitlab-odx.oracle.com/odx/functions/api/runner"
"gitlab-odx.oracle.com/odx/functions/api/server/internal/routecache"
) )
var tmpDatastoreTests = "/tmp/func_test_datastore.db" var tmpDatastoreTests = "/tmp/func_test_datastore.db"
@@ -31,7 +32,7 @@ func testServer(ds models.Datastore, mq models.MessageQueue, logDB models.FnLog,
LogDB: nil, LogDB: nil,
MQ: mq, MQ: mq,
Enqueue: DefaultEnqueue, Enqueue: DefaultEnqueue,
hotroutes: routecache.New(2), routeCache: cache.New(60*time.Second, 5*time.Minute),
} }
r := s.Router r := s.Router
@@ -102,7 +103,6 @@ func TestFullStack(t *testing.T) {
defer rnrcancel() defer rnrcancel()
srv := testServer(ds, &mqs.Mock{}, logDB, rnr) srv := testServer(ds, &mqs.Mock{}, logDB, rnr)
srv.hotroutes = routecache.New(2)
for _, test := range []struct { for _, test := range []struct {
name string name string
@@ -115,13 +115,15 @@ func TestFullStack(t *testing.T) {
{"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusOK, 0}, {"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusOK, 0},
{"list apps", "GET", "/v1/apps", ``, http.StatusOK, 0}, {"list apps", "GET", "/v1/apps", ``, http.StatusOK, 0},
{"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK, 0}, {"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK, 0},
{"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "funcy/hello" } }`, http.StatusOK, 1}, // NOTE: cache is lazy, loads when a request comes in for the route, not when added
{"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "funcy/error" } }`, http.StatusOK, 2}, {"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "funcy/hello" } }`, http.StatusOK, 0},
{"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 2}, {"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "funcy/error" } }`, http.StatusOK, 0},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 2}, {"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 0},
{"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK, 2}, {"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
{"execute myroute", "POST", "/r/myapp/myroute", `{ "name": "Teste" }`, http.StatusOK, 2}, {"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK, 0},
{"execute myroute", "POST", "/r/myapp/myroute", `{ "name": "Teste" }`, http.StatusOK, 1},
{"execute myroute2", "POST", "/r/myapp/myroute2", `{ "name": "Teste" }`, http.StatusInternalServerError, 2}, {"execute myroute2", "POST", "/r/myapp/myroute2", `{ "name": "Teste" }`, http.StatusInternalServerError, 2},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 2},
{"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 1}, {"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 1},
{"delete app (fail)", "DELETE", "/v1/apps/myapp", ``, http.StatusConflict, 1}, {"delete app (fail)", "DELETE", "/v1/apps/myapp", ``, http.StatusConflict, 1},
{"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0}, {"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
@@ -136,10 +138,10 @@ func TestFullStack(t *testing.T) {
t.Errorf("Test \"%s\": Expected status code to be %d but was %d", t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
test.name, test.expectedCode, rec.Code) test.name, test.expectedCode, rec.Code)
} }
if srv.hotroutes.Len() != test.expectedCacheSize { if srv.routeCache.ItemCount() != test.expectedCacheSize {
t.Log(buf.String()) t.Log(buf.String())
t.Errorf("Test \"%s\": Expected cache size to be %d but was %d", t.Errorf("Test \"%s\": Expected cache size to be %d but was %d",
test.name, test.expectedCacheSize, srv.hotroutes.Len()) test.name, test.expectedCacheSize, srv.routeCache.ItemCount())
} }
} }
} }

View File

@@ -4,8 +4,6 @@ package server
import ( import (
"sync" "sync"
"gitlab-odx.oracle.com/odx/functions/api/models"
) )
// call is an in-flight or completed do call // call is an in-flight or completed do call
@@ -17,17 +15,17 @@ type call struct {
type singleflight struct { type singleflight struct {
mu sync.Mutex // protects m mu sync.Mutex // protects m
m map[models.RouteFilter]*call // lazily initialized m map[interface{}]*call // lazily initialized
} }
// do executes and returns the results of the given function, making // do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a // sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the // time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results. // original to complete and receives the same results.
func (g *singleflight) do(key models.RouteFilter, fn func() (interface{}, error)) (interface{}, error) { func (g *singleflight) do(key interface{}, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock() g.mu.Lock()
if g.m == nil { if g.m == nil {
g.m = make(map[models.RouteFilter]*call) g.m = make(map[interface{}]*call)
} }
if c, ok := g.m[key]; ok { if c, ok := g.m[key]; ok {
g.mu.Unlock() g.mu.Unlock()

6
glide.lock generated
View File

@@ -1,5 +1,5 @@
hash: 91e44ada5fc186bf55c6e794c2ea7733ae1b915e850261e67f0e2ed1ba55eb8b hash: ed88f1a46f149bac3eea6052d409a2a619f762ee51f2655b3fc22e8b2fa806ad
updated: 2017-07-17T12:03:47.884564997-07:00 updated: 2017-07-19T22:11:29.697513445-07:00
imports: imports:
- name: code.cloudfoundry.org/bytefmt - name: code.cloudfoundry.org/bytefmt
version: f4415fafc5619dd75599a54a7c91fb3948ad58bd version: f4415fafc5619dd75599a54a7c91fb3948ad58bd
@@ -201,6 +201,8 @@ imports:
subpackages: subpackages:
- libcontainer/system - libcontainer/system
- libcontainer/user - libcontainer/user
- name: github.com/patrickmn/go-cache
version: 7ac151875ffb48b9f3ccce9ea20f020b0c1596c8
- name: github.com/pelletier/go-buffruneio - name: github.com/pelletier/go-buffruneio
version: c37440a7cf42ac63b919c752ca73a85067e05992 version: c37440a7cf42ac63b919c752ca73a85067e05992
- name: github.com/pelletier/go-toml - name: github.com/pelletier/go-toml

View File

@@ -72,4 +72,6 @@ testImport:
- package: github.com/vrischmann/envconfig - package: github.com/vrischmann/envconfig
- package: github.com/opencontainers/go-digest - package: github.com/opencontainers/go-digest
branch: master branch: master
- package: github.com/patrickmn/go-cache
branch: master

9
vendor/github.com/patrickmn/go-cache/CONTRIBUTORS generated vendored Normal file
View File

@@ -0,0 +1,9 @@
This is a list of people who have contributed code to go-cache. They, or their
employers, are the copyright holders of the contributed code. Contributed code
is subject to the license restrictions listed in LICENSE (as they were when the
code was contributed.)
Dustin Sallings <dustin@spy.net>
Jason Mooberry <jasonmoo@me.com>
Sergey Shepelev <temotor@gmail.com>
Alex Edwards <ajmedwards@gmail.com>

19
vendor/github.com/patrickmn/go-cache/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2012-2017 Patrick Mylund Nielsen and the go-cache contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

83
vendor/github.com/patrickmn/go-cache/README.md generated vendored Normal file
View File

@@ -0,0 +1,83 @@
# go-cache
go-cache is an in-memory key:value store/cache similar to memcached that is
suitable for applications running on a single machine. Its major advantage is
that, being essentially a thread-safe `map[string]interface{}` with expiration
times, it doesn't need to serialize or transmit its contents over the network.
Any object can be stored, for a given duration or forever, and the cache can be
safely used by multiple goroutines.
Although go-cache isn't meant to be used as a persistent datastore, the entire
cache can be saved to and loaded from a file (using `c.Items()` to retrieve the
items map to serialize, and `NewFrom()` to create a cache from a deserialized
one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats.)
### Installation
`go get github.com/patrickmn/go-cache`
### Usage
```go
import (
"fmt"
"github.com/patrickmn/go-cache"
"time"
)
func main() {
// Create a cache with a default expiration time of 5 minutes, and which
// purges expired items every 10 minutes
c := cache.New(5*time.Minute, 10*time.Minute)
// Set the value of the key "foo" to "bar", with the default expiration time
c.Set("foo", "bar", cache.DefaultExpiration)
// Set the value of the key "baz" to 42, with no expiration time
// (the item won't be removed until it is re-set, or removed using
// c.Delete("baz")
c.Set("baz", 42, cache.NoExpiration)
// Get the string associated with the key "foo" from the cache
foo, found := c.Get("foo")
if found {
fmt.Println(foo)
}
// Since Go is statically typed, and cache values can be anything, type
// assertion is needed when values are being passed to functions that don't
// take arbitrary types, (i.e. interface{}). The simplest way to do this for
// values which will only be used once--e.g. for passing to another
// function--is:
foo, found := c.Get("foo")
if found {
MyFunction(foo.(string))
}
// This gets tedious if the value is used several times in the same function.
// You might do either of the following instead:
if x, found := c.Get("foo"); found {
foo := x.(string)
// ...
}
// or
var foo string
if x, found := c.Get("foo"); found {
foo = x.(string)
}
// ...
// foo can then be passed around freely as a string
// Want performance? Store pointers!
c.Set("foo", &MyStruct, cache.DefaultExpiration)
if x, found := c.Get("foo"); found {
foo := x.(*MyStruct)
// ...
}
}
```
### Reference
`godoc` or [http://godoc.org/github.com/patrickmn/go-cache](http://godoc.org/github.com/patrickmn/go-cache)

1161
vendor/github.com/patrickmn/go-cache/cache.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

1771
vendor/github.com/patrickmn/go-cache/cache_test.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

192
vendor/github.com/patrickmn/go-cache/sharded.go generated vendored Normal file
View File

@@ -0,0 +1,192 @@
package cache
import (
"crypto/rand"
"math"
"math/big"
insecurerand "math/rand"
"os"
"runtime"
"time"
)
// This is an experimental and unexported (for now) attempt at making a cache
// with better algorithmic complexity than the standard one, namely by
// preventing write locks of the entire cache when an item is added. As of the
// time of writing, the overhead of selecting buckets results in cache
// operations being about twice as slow as for the standard cache with small
// total cache sizes, and faster for larger ones.
//
// See cache_test.go for a few benchmarks.
type unexportedShardedCache struct {
*shardedCache
}
type shardedCache struct {
seed uint32
m uint32
cs []*cache
janitor *shardedJanitor
}
// djb2 with better shuffling. 5x faster than FNV with the hash.Hash overhead.
func djb33(seed uint32, k string) uint32 {
var (
l = uint32(len(k))
d = 5381 + seed + l
i = uint32(0)
)
// Why is all this 5x faster than a for loop?
if l >= 4 {
for i < l-4 {
d = (d * 33) ^ uint32(k[i])
d = (d * 33) ^ uint32(k[i+1])
d = (d * 33) ^ uint32(k[i+2])
d = (d * 33) ^ uint32(k[i+3])
i += 4
}
}
switch l - i {
case 1:
case 2:
d = (d * 33) ^ uint32(k[i])
case 3:
d = (d * 33) ^ uint32(k[i])
d = (d * 33) ^ uint32(k[i+1])
case 4:
d = (d * 33) ^ uint32(k[i])
d = (d * 33) ^ uint32(k[i+1])
d = (d * 33) ^ uint32(k[i+2])
}
return d ^ (d >> 16)
}
func (sc *shardedCache) bucket(k string) *cache {
return sc.cs[djb33(sc.seed, k)%sc.m]
}
func (sc *shardedCache) Set(k string, x interface{}, d time.Duration) {
sc.bucket(k).Set(k, x, d)
}
func (sc *shardedCache) Add(k string, x interface{}, d time.Duration) error {
return sc.bucket(k).Add(k, x, d)
}
func (sc *shardedCache) Replace(k string, x interface{}, d time.Duration) error {
return sc.bucket(k).Replace(k, x, d)
}
func (sc *shardedCache) Get(k string) (interface{}, bool) {
return sc.bucket(k).Get(k)
}
func (sc *shardedCache) Increment(k string, n int64) error {
return sc.bucket(k).Increment(k, n)
}
func (sc *shardedCache) IncrementFloat(k string, n float64) error {
return sc.bucket(k).IncrementFloat(k, n)
}
func (sc *shardedCache) Decrement(k string, n int64) error {
return sc.bucket(k).Decrement(k, n)
}
func (sc *shardedCache) Delete(k string) {
sc.bucket(k).Delete(k)
}
func (sc *shardedCache) DeleteExpired() {
for _, v := range sc.cs {
v.DeleteExpired()
}
}
// Returns the items in the cache. This may include items that have expired,
// but have not yet been cleaned up. If this is significant, the Expiration
// fields of the items should be checked. Note that explicit synchronization
// is needed to use a cache and its corresponding Items() return values at
// the same time, as the maps are shared.
func (sc *shardedCache) Items() []map[string]Item {
res := make([]map[string]Item, len(sc.cs))
for i, v := range sc.cs {
res[i] = v.Items()
}
return res
}
func (sc *shardedCache) Flush() {
for _, v := range sc.cs {
v.Flush()
}
}
type shardedJanitor struct {
Interval time.Duration
stop chan bool
}
func (j *shardedJanitor) Run(sc *shardedCache) {
j.stop = make(chan bool)
tick := time.Tick(j.Interval)
for {
select {
case <-tick:
sc.DeleteExpired()
case <-j.stop:
return
}
}
}
func stopShardedJanitor(sc *unexportedShardedCache) {
sc.janitor.stop <- true
}
func runShardedJanitor(sc *shardedCache, ci time.Duration) {
j := &shardedJanitor{
Interval: ci,
}
sc.janitor = j
go j.Run(sc)
}
func newShardedCache(n int, de time.Duration) *shardedCache {
max := big.NewInt(0).SetUint64(uint64(math.MaxUint32))
rnd, err := rand.Int(rand.Reader, max)
var seed uint32
if err != nil {
os.Stderr.Write([]byte("WARNING: go-cache's newShardedCache failed to read from the system CSPRNG (/dev/urandom or equivalent.) Your system's security may be compromised. Continuing with an insecure seed.\n"))
seed = insecurerand.Uint32()
} else {
seed = uint32(rnd.Uint64())
}
sc := &shardedCache{
seed: seed,
m: uint32(n),
cs: make([]*cache, n),
}
for i := 0; i < n; i++ {
c := &cache{
defaultExpiration: de,
items: map[string]Item{},
}
sc.cs[i] = c
}
return sc
}
func unexportedNewSharded(defaultExpiration, cleanupInterval time.Duration, shards int) *unexportedShardedCache {
if defaultExpiration == 0 {
defaultExpiration = -1
}
sc := newShardedCache(shards, defaultExpiration)
SC := &unexportedShardedCache{sc}
if cleanupInterval > 0 {
runShardedJanitor(sc, cleanupInterval)
runtime.SetFinalizer(SC, stopShardedJanitor)
}
return SC
}

85
vendor/github.com/patrickmn/go-cache/sharded_test.go generated vendored Normal file
View File

@@ -0,0 +1,85 @@
package cache
import (
"strconv"
"sync"
"testing"
"time"
)
// func TestDjb33(t *testing.T) {
// }
var shardedKeys = []string{
"f",
"fo",
"foo",
"barf",
"barfo",
"foobar",
"bazbarf",
"bazbarfo",
"bazbarfoo",
"foobarbazq",
"foobarbazqu",
"foobarbazquu",
"foobarbazquux",
}
func TestShardedCache(t *testing.T) {
tc := unexportedNewSharded(DefaultExpiration, 0, 13)
for _, v := range shardedKeys {
tc.Set(v, "value", DefaultExpiration)
}
}
func BenchmarkShardedCacheGetExpiring(b *testing.B) {
benchmarkShardedCacheGet(b, 5*time.Minute)
}
func BenchmarkShardedCacheGetNotExpiring(b *testing.B) {
benchmarkShardedCacheGet(b, NoExpiration)
}
func benchmarkShardedCacheGet(b *testing.B, exp time.Duration) {
b.StopTimer()
tc := unexportedNewSharded(exp, 0, 10)
tc.Set("foobarba", "zquux", DefaultExpiration)
b.StartTimer()
for i := 0; i < b.N; i++ {
tc.Get("foobarba")
}
}
func BenchmarkShardedCacheGetManyConcurrentExpiring(b *testing.B) {
benchmarkShardedCacheGetManyConcurrent(b, 5*time.Minute)
}
func BenchmarkShardedCacheGetManyConcurrentNotExpiring(b *testing.B) {
benchmarkShardedCacheGetManyConcurrent(b, NoExpiration)
}
func benchmarkShardedCacheGetManyConcurrent(b *testing.B, exp time.Duration) {
b.StopTimer()
n := 10000
tsc := unexportedNewSharded(exp, 0, 20)
keys := make([]string, n)
for i := 0; i < n; i++ {
k := "foo" + strconv.Itoa(n)
keys[i] = k
tsc.Set(k, "bar", DefaultExpiration)
}
each := b.N / n
wg := new(sync.WaitGroup)
wg.Add(n)
for _, v := range keys {
go func() {
for j := 0; j < each; j++ {
tsc.Get(v)
}
wg.Done()
}()
}
b.StartTimer()
wg.Wait()
}