moving caching w/ TTL to its own package

This commit is contained in:
Brad Rydzewski 2015-10-20 16:45:24 -07:00
parent efcab1210b
commit eb04d418d9
14 changed files with 275 additions and 112 deletions

39
cache/cache.go vendored Normal file
View file

@ -0,0 +1,39 @@
package cache
import (
"time"
"github.com/koding/cache"
"golang.org/x/net/context"
)
type Cache interface {
Get(string) (interface{}, error)
Set(string, interface{}) error
}
func Get(c context.Context, key string) (interface{}, error) {
return FromContext(c).Get(key)
}
func Set(c context.Context, key string, value interface{}) error {
return FromContext(c).Set(key, value)
}
// Default creates an in-memory cache with the default
// 24 hour expiration period.
func Default() Cache {
return cache.NewMemoryWithTTL(time.Hour * 24)
}
// NewTTL returns an in-memory cache with the specified
// ttl expiration period.
func NewTTL(t time.Duration) Cache {
return cache.NewMemoryWithTTL(t)
}
// NewTTL returns an in-memory cache with the specified
// ttl expiration period.
func NewLRU(size int) Cache {
return cache.NewLRU(size)
}

35
cache/cache_test.go vendored Normal file
View file

@ -0,0 +1,35 @@
package cache
import (
"testing"
// "github.com/drone/drone/model"
"github.com/franela/goblin"
"github.com/gin-gonic/gin"
)
func TestCache(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Cache", func() {
var c *gin.Context
g.BeforeEach(func() {
c = new(gin.Context)
ToContext(c, Default())
})
g.It("Should set and get an item", func() {
Set(c, "foo", "bar")
v, e := Get(c, "foo")
g.Assert(v).Equal("bar")
g.Assert(e == nil).IsTrue()
})
g.It("Should return nil when item not found", func() {
v, e := Get(c, "foo")
g.Assert(v == nil).IsTrue()
g.Assert(e == nil).IsFalse()
})
})
}

23
cache/context.go vendored Normal file
View file

@ -0,0 +1,23 @@
package cache
import (
"golang.org/x/net/context"
)
const key = "cache"
// Setter defines a context that enables setting values.
type Setter interface {
Set(string, interface{})
}
// FromContext returns the Cache associated with this context.
func FromContext(c context.Context) Cache {
return c.Value(key).(Cache)
}
// ToContext adds the Cache to this context if it supports
// the Setter interface.
func ToContext(c Setter, cache Cache) {
c.Set(key, cache)
}

75
cache/helper.go vendored Normal file
View file

@ -0,0 +1,75 @@
package cache
import (
"fmt"
"github.com/drone/drone/model"
"golang.org/x/net/context"
)
// GetRepos returns the user permissions to the named repository
// from the cache associated with the current context.
func GetPerms(c context.Context, user *model.User, owner, name string) *model.Perm {
key := fmt.Sprintf("perms:%s:%s/%s",
user.Login,
owner,
name,
)
val, err := FromContext(c).Get(key)
if err != nil {
return nil
}
return val.(*model.Perm)
}
// SetRepos adds the listof user permissions to the named repsotiory
// to the cache assocaited with the current context.
func SetPerms(c context.Context, user *model.User, perm *model.Perm, owner, name string) {
key := fmt.Sprintf("perms:%s:%s/%s",
user.Login,
owner,
name,
)
FromContext(c).Set(key, perm)
}
// GetRepos returns the list of user repositories from the cache
// associated with the current context.
func GetRepos(c context.Context, user *model.User) []*model.RepoLite {
key := fmt.Sprintf("repos:%s",
user.Login,
)
val, err := FromContext(c).Get(key)
if err != nil {
return nil
}
return val.([]*model.RepoLite)
}
// SetRepos adds the listof user repositories to the cache assocaited
// with the current context.
func SetRepos(c context.Context, user *model.User, repos []*model.RepoLite) {
key := fmt.Sprintf("repos:%s",
user.Login,
)
FromContext(c).Set(key, repos)
}
// GetSetRepos is a helper function that attempts to get the
// repository list from the cache first. If no data is in the
// cache or it is expired, it will remotely fetch the list of
// repositories and populate the cache.
// func GetSetRepos(c context.Context, user *model.User) ([]*model.RepoLite, error) {
// cache := FromContext(c).Repos()
// repos := FromContext(c).Repos().Get(user)
// if repos != nil {
// return repos, nil
// }
// var err error
// repos, err = remote.FromContext(c).Repos(user)
// if err != nil {
// return nil, err
// }
// cache.Set(user, repos)
// return repos, nil
// }

56
cache/helper_test.go vendored Normal file
View file

@ -0,0 +1,56 @@
package cache
import (
"testing"
"github.com/drone/drone/model"
"github.com/franela/goblin"
"github.com/gin-gonic/gin"
)
func TestHelper(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Cache helpers", func() {
var c *gin.Context
g.BeforeEach(func() {
c = new(gin.Context)
ToContext(c, Default())
})
g.It("Should set and get permissions", func() {
SetPerms(c, fakeUser, fakePerm, "octocat", "Spoon-Knife")
v := GetPerms(c, fakeUser, "octocat", "Spoon-Knife")
g.Assert(v).Equal(fakePerm)
})
g.It("Should return nil if permissions if not found", func() {
v := GetPerms(c, fakeUser, "octocat", "Spoon-Knife")
g.Assert(v == nil).IsTrue()
})
g.It("Should set and get repositories", func() {
SetRepos(c, fakeUser, fakeRepos)
v := GetRepos(c, fakeUser)
g.Assert(v).Equal(fakeRepos)
})
g.It("Should return nil if repositories not found", func() {
v := GetRepos(c, fakeUser)
g.Assert(v == nil).IsTrue()
})
})
}
var (
fakeUser = &model.User{Login: "octocat"}
fakePerm = &model.Perm{true, true, true}
fakeRepos = []*model.RepoLite{
{Owner: "octocat", Name: "Hello-World"},
{Owner: "octocat", Name: "hello-world"},
{Owner: "octocat", Name: "Spoon-Knife"},
}
)

View file

@ -40,3 +40,10 @@ Please use `http://drone.mycompany.com/authorize` as the Authorization callback
* Team Membership:Read * Team Membership:Read
* Repositories:Read * Repositories:Read
* Webhooks:Read and Write * Webhooks:Read and Write
## Known Issues
This section details known issues and planned features:
* Pull Request support
* Mercurial support

View file

@ -6,7 +6,7 @@ Drone comes with support for MySQL as an alternate database engine. To enable My
```bash ```bash
DATABASE_DRIVER="mysql" DATABASE_DRIVER="mysql"
DATABASE_CONFIG="root:pa55word@tcp(localhost:3306)/drone" DATABASE_CONFIG="root:pa55word@tcp(localhost:3306)/drone?parseTime=true"
``` ```
## MySQL configuration ## MySQL configuration
@ -29,12 +29,12 @@ The components of this string are:
This is an example connection string: This is an example connection string:
``` ```
root:pa55word@tcp(localhost:3306)/drone root:pa55word@tcp(localhost:3306)/drone?parseTime=true
``` ```
## MySQL options ## MySQL options
See the official [driver documentation](https://github.com/go-sql-driver/mysql#parameters) for a full list of driver options. See the official [driver documentation](https://github.com/go-sql-driver/mysql#parameters) for a full list of driver options. Note that the `parseTime=true` is required.
## MySQL Database ## MySQL Database

View file

@ -6,6 +6,7 @@ import (
"github.com/drone/drone/engine" "github.com/drone/drone/engine"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/router" "github.com/drone/drone/router"
"github.com/drone/drone/router/middleware/cache"
"github.com/drone/drone/router/middleware/context" "github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/header" "github.com/drone/drone/router/middleware/header"
"github.com/drone/drone/shared/database" "github.com/drone/drone/shared/database"
@ -49,6 +50,7 @@ func main() {
server_.Run( server_.Run(
router.Load( router.Load(
header.Version(build), header.Version(build),
cache.Default(),
context.SetDatabase(database_), context.SetDatabase(database_),
context.SetRemote(remote_), context.SetRemote(remote_),
context.SetEngine(engine_), context.SetEngine(engine_),

View file

@ -1,49 +1,14 @@
package cache package cache
import ( import (
"time" "github.com/drone/drone/cache"
"github.com/gin-gonic/gin"
"github.com/hashicorp/golang-lru"
) )
// single instance of a thread-safe lru cache func Default() gin.HandlerFunc {
var cache *lru.Cache cc := cache.Default()
return func(c *gin.Context) {
func init() { cache.ToContext(c, cc)
var err error c.Next()
cache, err = lru.New(2048)
if err != nil {
panic(err)
} }
} }
// item is a simple wrapper around a cacheable object
// that tracks the ttl for item expiration in the cache.
type item struct {
value interface{}
ttl time.Time
}
// set adds the key value pair to the cache with the
// specified ttl expiration.
func set(key string, value interface{}, ttl int64) {
ttlv := time.Now().Add(time.Duration(ttl) * time.Second)
cache.Add(key, &item{value, ttlv})
}
// get gets the value from the cache for the given key.
// if the value does not exist, a nil value is returned.
// if the value exists, but is expired, the value is returned
// with a bool flag set to true.
func get(key string) (interface{}, bool) {
v, ok := cache.Get(key)
if !ok {
return nil, false
}
vv := v.(*item)
expired := vv.ttl.Before(time.Now())
if expired {
cache.Remove(key)
}
return vv.value, expired
}

View file

@ -1,40 +0,0 @@
package cache
import (
"testing"
"github.com/franela/goblin"
)
func TestCache(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Cache", func() {
g.BeforeEach(func() {
cache.Purge()
})
g.It("should set and get item", func() {
set("foo", "bar", 1000)
val, expired := get("foo")
g.Assert(val).Equal("bar")
g.Assert(expired).Equal(false)
})
g.It("should return nil when item not found", func() {
val, expired := get("foo")
g.Assert(val == nil).IsTrue()
g.Assert(expired).Equal(false)
})
g.It("should get expired item and purge", func() {
set("foo", "bar", -900)
val, expired := get("foo")
g.Assert(val).Equal("bar")
g.Assert(expired).Equal(true)
val, _ = get("foo")
g.Assert(val == nil).IsTrue()
})
})
}

View file

@ -1,8 +1,7 @@
package cache package cache
import ( import (
"fmt" "github.com/drone/drone/cache"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -24,16 +23,14 @@ func Perms(c *gin.Context) {
return return
} }
key := fmt.Sprintf("perm/%s/%s/%s",
user.(*model.User).Login,
owner,
name,
)
// if the item already exists in the cache // if the item already exists in the cache
// we can continue the middleware chain and // we can continue the middleware chain and
// exit afterwards. // exit afterwards.
v, _ := get(key) v := cache.GetPerms(c,
user.(*model.User),
owner,
name,
)
if v != nil { if v != nil {
c.Set("perm", v) c.Set("perm", v)
c.Next() c.Next()
@ -47,6 +44,11 @@ func Perms(c *gin.Context) {
perm, ok := c.Get("perm") perm, ok := c.Get("perm")
if ok { if ok {
set(key, perm, 86400) // 24 hours cache.SetPerms(c,
user.(*model.User),
perm.(*model.Perm),
owner,
name,
)
} }
} }

View file

@ -3,6 +3,7 @@ package cache
import ( import (
"testing" "testing"
"github.com/drone/drone/cache"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/franela/goblin" "github.com/franela/goblin"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -13,12 +14,13 @@ func TestPermCache(t *testing.T) {
g := goblin.Goblin(t) g := goblin.Goblin(t)
g.Describe("Perm Cache", func() { g.Describe("Perm Cache", func() {
var c *gin.Context
g.BeforeEach(func() { g.BeforeEach(func() {
cache.Purge() c = new(gin.Context)
cache.ToContext(c, cache.Default())
}) })
g.It("should skip when no user session", func() { g.It("should skip when no user session", func() {
c := &gin.Context{}
c.Params = gin.Params{ c.Params = gin.Params{
gin.Param{Key: "owner", Value: "octocat"}, gin.Param{Key: "owner", Value: "octocat"},
gin.Param{Key: "name", Value: "hello-world"}, gin.Param{Key: "name", Value: "hello-world"},
@ -31,13 +33,12 @@ func TestPermCache(t *testing.T) {
}) })
g.It("should get perms from cache", func() { g.It("should get perms from cache", func() {
c := &gin.Context{}
c.Params = gin.Params{ c.Params = gin.Params{
gin.Param{Key: "owner", Value: "octocat"}, gin.Param{Key: "owner", Value: "octocat"},
gin.Param{Key: "name", Value: "hello-world"}, gin.Param{Key: "name", Value: "hello-world"},
} }
c.Set("user", fakeUser) c.Set("user", fakeUser)
set("perm/octocat/octocat/hello-world", fakePerm, 999) cache.SetPerms(c, fakeUser, fakePerm, "octocat", "hello-world")
Perms(c) Perms(c)

View file

@ -1,8 +1,7 @@
package cache package cache
import ( import (
"fmt" "github.com/drone/drone/cache"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -18,14 +17,10 @@ func Repos(c *gin.Context) {
return return
} }
key := fmt.Sprintf("repos/%s",
user.(*model.User).Login,
)
// if the item already exists in the cache // if the item already exists in the cache
// we can continue the middleware chain and // we can continue the middleware chain and
// exit afterwards. // exit afterwards.
v, _ := get(key) v := cache.GetRepos(c, user.(*model.User))
if v != nil { if v != nil {
c.Set("repos", v) c.Set("repos", v)
c.Next() c.Next()
@ -39,6 +34,9 @@ func Repos(c *gin.Context) {
repos, ok := c.Get("repos") repos, ok := c.Get("repos")
if ok { if ok {
set(key, repos, 86400) // 24 hours cache.SetRepos(c,
user.(*model.User),
repos.([]*model.RepoLite),
)
} }
} }

View file

@ -3,6 +3,7 @@ package cache
import ( import (
"testing" "testing"
"github.com/drone/drone/cache"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/franela/goblin" "github.com/franela/goblin"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -13,13 +14,13 @@ func TestReposCache(t *testing.T) {
g := goblin.Goblin(t) g := goblin.Goblin(t)
g.Describe("Repo List Cache", func() { g.Describe("Repo List Cache", func() {
var c *gin.Context
g.BeforeEach(func() { g.BeforeEach(func() {
cache.Purge() c = new(gin.Context)
cache.ToContext(c, cache.Default())
}) })
g.It("should skip when no user session", func() { g.It("should skip when no user session", func() {
c := &gin.Context{}
Perms(c) Perms(c)
_, ok := c.Get("perm") _, ok := c.Get("perm")
@ -27,9 +28,8 @@ func TestReposCache(t *testing.T) {
}) })
g.It("should get repos from cache", func() { g.It("should get repos from cache", func() {
c := &gin.Context{}
c.Set("user", fakeUser) c.Set("user", fakeUser)
set("repos/octocat", fakeRepos, 999) cache.SetRepos(c, fakeUser, fakeRepos)
Repos(c) Repos(c)