diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go new file mode 100644 index 00000000000..8286f65fea6 --- /dev/null +++ b/pkg/infra/distcache/database_storage.go @@ -0,0 +1,82 @@ +package distcache + +import ( + "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type databaseCache struct { + SQLStore *sqlstore.SqlStore +} + +var getTime = time.Now + +func (dc *databaseCache) Get(key string) (interface{}, error) { + //now := getTime().Unix() + + cacheHits := []CacheData{} + err := dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) + if err != nil { + return nil, err + } + + var cacheHit CacheData + if len(cacheHits) == 0 { + return nil, ErrCacheItemNotFound + } + + cacheHit = cacheHits[0] + if cacheHit.Expires > 0 { + if getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires { + dc.Delete(key) + return nil, ErrCacheItemNotFound + } + } + + item := &Item{} + if err = DecodeGob(cacheHit.Data, item); err != nil { + return nil, err + } + + return item.Val, nil +} + +type CacheData struct { + Key string + Data []byte + Expires int64 + CreatedAt int64 +} + +func (dc *databaseCache) Put(key string, value interface{}, expire int64) error { + item := &Item{Val: value} + data, err := EncodeGob(item) + if err != nil { + return err + } + + now := getTime().Unix() + + cacheHits := []CacheData{} + err = dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) + if err != nil { + return err + } + + if len(cacheHits) > 0 { + _, err = dc.SQLStore.NewSession().Exec("UPDATE cached_data SET data=?, created=?, expire=? WHERE key=?", data, now, expire, key) + } else { + _, err = dc.SQLStore.NewSession().Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expire) + } + + return err +} + +func (dc *databaseCache) Delete(key string) error { + sql := `DELETE FROM cache_data WHERE key = ?` + + _, err := dc.SQLStore.NewSession().Exec(sql, key) + + return err +} diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go new file mode 100644 index 00000000000..11efd435de3 --- /dev/null +++ b/pkg/infra/distcache/distcache.go @@ -0,0 +1,68 @@ +package distcache + +import ( + "bytes" + "encoding/gob" + "errors" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + + "github.com/grafana/grafana/pkg/registry" +) + +var ( + ErrCacheItemNotFound = errors.New("cache item not found") +) + +func init() { + registry.RegisterService(&DistributedCache{}) +} + +// Init initializes the service +func (ds *DistributedCache) Init() error { + ds.log = log.New("distributed.cache") + + // memory + // redis + // memcache + // database. using SQLSTORE + ds.Client = &databaseCache{SQLStore: ds.SQLStore} + + return nil +} + +// DistributedCache allows Grafana to cache data outside its own process +type DistributedCache struct { + log log.Logger + Client cacheStorage + SQLStore *sqlstore.SqlStore `inject:""` +} + +type Item struct { + Val interface{} + Created int64 + Expire int64 +} + +func EncodeGob(item *Item) ([]byte, error) { + buf := bytes.NewBuffer(nil) + err := gob.NewEncoder(buf).Encode(item) + return buf.Bytes(), err +} + +func DecodeGob(data []byte, out *Item) error { + buf := bytes.NewBuffer(data) + return gob.NewDecoder(buf).Decode(&out) +} + +type cacheStorage interface { + // Get reads object from Cache + Get(key string) (interface{}, error) + + // Puts an object into the cache + Put(key string, value interface{}, expire int64) error + + // Delete object from cache + Delete(key string) error +} diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go new file mode 100644 index 00000000000..88066daec7e --- /dev/null +++ b/pkg/infra/distcache/distcache_test.go @@ -0,0 +1,87 @@ +package distcache + +import ( + "encoding/gob" + "testing" + "time" + + "github.com/bmizerany/assert" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type CacheableStruct struct { + String string + Int64 int64 +} + +func init() { + gob.Register(CacheableStruct{}) +} + +func createClient(t *testing.T) cacheStorage { + t.Helper() + + sqlstore := sqlstore.InitTestDB(t) + dc := DistributedCache{log: log.New("test.logger"), SQLStore: sqlstore} + dc.Init() + return dc.Client +} + +func TestCanPutIntoDatabaseStorage(t *testing.T) { + client := createClient(t) + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + err := client.Put("key", cacheableStruct, 1000) + assert.Equal(t, err, nil) + + data, err := client.Get("key") + s, ok := data.(CacheableStruct) + + assert.Equal(t, ok, true) + assert.Equal(t, s.String, "hej") + assert.Equal(t, s.Int64, int64(2000)) + + err = client.Delete("key") + assert.Equal(t, err, nil) + + _, err = client.Get("key") + assert.Equal(t, err, ErrCacheItemNotFound) +} + +func TestCanNotFetchExpiredItems(t *testing.T) { + client := createClient(t) + + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + // insert cache item one day back + getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + err := client.Put("key", cacheableStruct, 10000) + assert.Equal(t, err, nil) + + // should not be able to read that value since its expired + getTime = time.Now + _, err = client.Get("key") + assert.Equal(t, err, ErrCacheItemNotFound) +} + +func TestCanSetInfiniteCacheExpiration(t *testing.T) { + client := createClient(t) + + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + // insert cache item one day back + getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + err := client.Put("key", cacheableStruct, 0) + assert.Equal(t, err, nil) + + // should not be able to read that value since its expired + getTime = time.Now + data, err := client.Get("key") + s, ok := data.(CacheableStruct) + + assert.Equal(t, ok, true) + assert.Equal(t, s.String, "hej") + assert.Equal(t, s.Int64, int64(2000)) +} diff --git a/pkg/services/sqlstore/migrations/cache_data_mig.go b/pkg/services/sqlstore/migrations/cache_data_mig.go new file mode 100644 index 00000000000..1201b38e337 --- /dev/null +++ b/pkg/services/sqlstore/migrations/cache_data_mig.go @@ -0,0 +1,17 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addCacheMigration(mg *Migrator) { + var cacheDataV1 = Table{ + Name: "cache_data", + Columns: []*Column{ + {Name: "key", Type: DB_Char, IsPrimaryKey: true, Length: 16}, + {Name: "data", Type: DB_Blob}, + {Name: "expires", Type: DB_Integer, Length: 255, Nullable: false}, + {Name: "created_at", Type: DB_Integer, Length: 255, Nullable: false}, + }, + } + + mg.AddMigration("create cache_data table", NewAddTableMigration(cacheDataV1)) +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 931259ec3ed..3e40c749f37 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -33,6 +33,7 @@ func AddMigrations(mg *Migrator) { addUserAuthMigrations(mg) addServerlockMigrations(mg) addUserAuthTokenMigrations(mg) + addCacheMigration(mg) } func addMigrationLogMigrations(mg *Migrator) {