diff --git a/.circleci/config.yml b/.circleci/config.yml index 69cea87dccd..da0e0665285 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,6 +56,20 @@ jobs: name: postgres integration tests command: './scripts/circle-test-postgres.sh' + cache-server-test: + docker: + - image: circleci/golang:1.11.5 + - image: circleci/redis:4-alpine + - image: memcached + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: dockerize -wait tcp://127.0.0.1:11211 -timeout 120s + - run: dockerize -wait tcp://127.0.0.1:6379 -timeout 120s + - run: + name: cache server tests + command: './scripts/circle-test-cache-servers.sh' + codespell: docker: - image: circleci/python @@ -545,6 +559,8 @@ workflows: filters: *filter-not-release-or-master - postgres-integration-test: filters: *filter-not-release-or-master + - cache-server-test: + filters: *filter-not-release-or-master - grafana-docker-pr: requires: - build @@ -554,4 +570,5 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test + - cache-server-test filters: *filter-not-release-or-master diff --git a/conf/defaults.ini b/conf/defaults.ini index 044d8e59a7a..6447c22bddc 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -106,6 +106,17 @@ path = grafana.db # For "sqlite3" only. cache mode setting used for connecting to the database cache_mode = private +#################################### Cache server ############################# +[remote_cache] +# Either "redis", "memcached" or "database" default is "database" +type = database + +# cache connectionstring options +# database: will use Grafana primary database. +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# memcache: 127.0.0.1:11211 +connstr = + #################################### Session ############################# [session] # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" diff --git a/conf/sample.ini b/conf/sample.ini index dc1e4fbde8e..1a574243f79 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -102,6 +102,17 @@ log_queries = # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) ;cache_mode = private +#################################### Cache server ############################# +[remote_cache] +# Either "redis", "memcached" or "database" default is "database" +;type = database + +# cache connectionstring options +# database: will use Grafana primary database. +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# memcache: 127.0.0.1:11211 +;connstr = + #################################### Session #################################### [session] # Either "memory", "file", "redis", "mysql", "postgres", default is "file" diff --git a/devenv/docker/blocks/redis/docker-compose.yaml b/devenv/docker/blocks/redis/docker-compose.yaml index 65071d4966b..fb56afaac1c 100644 --- a/devenv/docker/blocks/redis/docker-compose.yaml +++ b/devenv/docker/blocks/redis/docker-compose.yaml @@ -1,4 +1,4 @@ - memcached: + redis: image: redis:latest ports: - "6379:6379" diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 3d1b25979c3..d4ea0c05fb0 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -179,7 +179,6 @@ Path to the certificate key file (if `protocol` is set to `https`). Set to true for Grafana to log all HTTP requests (not just errors). These are logged as Info level events to grafana log. -

@@ -262,6 +261,19 @@ Set to `true` to log the sql calls and execution times. For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared) Defaults to private. +
+ +## [remote_cache] + +### type + +Either `redis`, `memcached` or `database` default is `database` + +### connstr + +The remote cache connection string. Leave empty when using `database` since it will use the primary database. +Redis example config: `addr=127.0.0.1:6379,pool_size=100,db=grafana` +Memcache example: `127.0.0.1:11211`
diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 53218147ae0..c10212329cf 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -29,6 +29,7 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/infra/metrics" + _ "github.com/grafana/grafana/pkg/infra/remotecache" _ "github.com/grafana/grafana/pkg/infra/serverlock" _ "github.com/grafana/grafana/pkg/infra/tracing" _ "github.com/grafana/grafana/pkg/infra/usagestats" diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go new file mode 100644 index 00000000000..1c39d74d800 --- /dev/null +++ b/pkg/infra/remotecache/database_storage.go @@ -0,0 +1,126 @@ +package remotecache + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +var getTime = time.Now + +const databaseCacheType = "database" + +type databaseCache struct { + SQLStore *sqlstore.SqlStore + log log.Logger +} + +func newDatabaseCache(sqlstore *sqlstore.SqlStore) *databaseCache { + dc := &databaseCache{ + SQLStore: sqlstore, + log: log.New("remotecache.database"), + } + + return dc +} + +func (dc *databaseCache) Run(ctx context.Context) error { + ticker := time.NewTicker(time.Minute * 10) + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + dc.internalRunGC() + } + } +} + +func (dc *databaseCache) internalRunGC() { + now := getTime().Unix() + sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0` + + _, err := dc.SQLStore.NewSession().Exec(sql, now) + if err != nil { + dc.log.Error("failed to run garbage collect", "error", err) + } +} + +func (dc *databaseCache) Get(key string) (interface{}, error) { + cacheHit := CacheData{} + session := dc.SQLStore.NewSession() + defer session.Close() + + exist, err := session.Where("cache_key= ?", key).Get(&cacheHit) + + if err != nil { + return nil, err + } + + if !exist { + return nil, ErrCacheItemNotFound + } + + if cacheHit.Expires > 0 { + existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires + if existedButExpired { + _ = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway + return nil, ErrCacheItemNotFound + } + } + + item := &cachedItem{} + if err = decodeGob(cacheHit.Data, item); err != nil { + return nil, err + } + + return item.Val, nil +} + +func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration) error { + item := &cachedItem{Val: value} + data, err := encodeGob(item) + if err != nil { + return err + } + + session := dc.SQLStore.NewSession() + + var cacheHit CacheData + has, err := session.Where("cache_key = ?", key).Get(&cacheHit) + if err != nil { + return err + } + + var expiresInSeconds int64 + if expire != 0 { + expiresInSeconds = int64(expire) / int64(time.Second) + } + + // insert or update depending on if item already exist + if has { + sql := `UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'` + _, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key) + } else { + sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)` + _, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds) + } + + return err +} + +func (dc *databaseCache) Delete(key string) error { + sql := "DELETE FROM cache_data WHERE cache_key=?" + _, err := dc.SQLStore.NewSession().Exec(sql, key) + + return err +} + +type CacheData struct { + CacheKey string + Data []byte + Expires int64 + CreatedAt int64 +} diff --git a/pkg/infra/remotecache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go new file mode 100644 index 00000000000..d15e26fd07f --- /dev/null +++ b/pkg/infra/remotecache/database_storage_test.go @@ -0,0 +1,56 @@ +package remotecache + +import ( + "testing" + "time" + + "github.com/bmizerany/assert" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func TestDatabaseStorageGarbageCollection(t *testing.T) { + sqlstore := sqlstore.InitTestDB(t) + + db := &databaseCache{ + SQLStore: sqlstore, + log: log.New("remotecache.database"), + } + + obj := &CacheableStruct{String: "foolbar"} + + //set time.now to 2 weeks ago + var err error + getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + err = db.Set("key1", obj, 1000*time.Second) + assert.Equal(t, err, nil) + + err = db.Set("key2", obj, 1000*time.Second) + assert.Equal(t, err, nil) + + err = db.Set("key3", obj, 1000*time.Second) + assert.Equal(t, err, nil) + + // insert object that should never expire + db.Set("key4", obj, 0) + + getTime = time.Now + db.Set("key5", obj, 1000*time.Second) + + //run GC + db.internalRunGC() + + //try to read values + _, err = db.Get("key1") + assert.Equal(t, err, ErrCacheItemNotFound, "expected cache item not found. got: ", err) + _, err = db.Get("key2") + assert.Equal(t, err, ErrCacheItemNotFound) + _, err = db.Get("key3") + assert.Equal(t, err, ErrCacheItemNotFound) + + _, err = db.Get("key4") + assert.Equal(t, err, nil) + _, err = db.Get("key5") + assert.Equal(t, err, nil) +} diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go new file mode 100644 index 00000000000..5424a05ad02 --- /dev/null +++ b/pkg/infra/remotecache/memcached_storage.go @@ -0,0 +1,71 @@ +package remotecache + +import ( + "time" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/grafana/grafana/pkg/setting" +) + +const memcachedCacheType = "memcached" + +type memcachedStorage struct { + c *memcache.Client +} + +func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage { + return &memcachedStorage{ + c: memcache.New(opts.ConnStr), + } +} + +func newItem(sid string, data []byte, expire int32) *memcache.Item { + return &memcache.Item{ + Key: sid, + Value: data, + Expiration: expire, + } +} + +// Set sets value to given key in the cache. +func (s *memcachedStorage) Set(key string, val interface{}, expires time.Duration) error { + item := &cachedItem{Val: val} + bytes, err := encodeGob(item) + if err != nil { + return err + } + + var expiresInSeconds int64 + if expires != 0 { + expiresInSeconds = int64(expires) / int64(time.Second) + } + + memcachedItem := newItem(key, bytes, int32(expiresInSeconds)) + return s.c.Set(memcachedItem) +} + +// Get gets value by given key in the cache. +func (s *memcachedStorage) Get(key string) (interface{}, error) { + memcachedItem, err := s.c.Get(key) + if err != nil && err.Error() == "memcache: cache miss" { + return nil, ErrCacheItemNotFound + } + + if err != nil { + return nil, err + } + + item := &cachedItem{} + + err = decodeGob(memcachedItem.Value, item) + if err != nil { + return nil, err + } + + return item.Val, nil +} + +// Delete delete a key from the cache +func (s *memcachedStorage) Delete(key string) error { + return s.c.Delete(key) +} diff --git a/pkg/infra/remotecache/memcached_storage_integration_test.go b/pkg/infra/remotecache/memcached_storage_integration_test.go new file mode 100644 index 00000000000..d1d82468644 --- /dev/null +++ b/pkg/infra/remotecache/memcached_storage_integration_test.go @@ -0,0 +1,15 @@ +// +build memcached + +package remotecache + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" +) + +func TestMemcachedCacheStorage(t *testing.T) { + opts := &setting.RemoteCacheOptions{Name: memcachedCacheType, ConnStr: "localhost:11211"} + client := createTestClient(t, opts, nil) + runTestsForClient(t, client) +} diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go new file mode 100644 index 00000000000..bd54b843119 --- /dev/null +++ b/pkg/infra/remotecache/redis_storage.go @@ -0,0 +1,62 @@ +package remotecache + +import ( + "time" + + "github.com/grafana/grafana/pkg/setting" + redis "gopkg.in/redis.v2" +) + +const redisCacheType = "redis" + +type redisStorage struct { + c *redis.Client +} + +func newRedisStorage(opts *setting.RemoteCacheOptions) *redisStorage { + opt := &redis.Options{ + Network: "tcp", + Addr: opts.ConnStr, + } + return &redisStorage{c: redis.NewClient(opt)} +} + +// Set sets value to given key in session. +func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) error { + item := &cachedItem{Val: val} + value, err := encodeGob(item) + if err != nil { + return err + } + + status := s.c.SetEx(key, expires, string(value)) + return status.Err() +} + +// Get gets value by given key in session. +func (s *redisStorage) Get(key string) (interface{}, error) { + v := s.c.Get(key) + + item := &cachedItem{} + err := decodeGob([]byte(v.Val()), item) + + if err == nil { + return item.Val, nil + } + + if err.Error() == "EOF" { + return nil, ErrCacheItemNotFound + } + + if err != nil { + return nil, err + } + + return item.Val, nil +} + +// Delete delete a key from session. +func (s *redisStorage) Delete(key string) error { + cmd := s.c.Del(key) + return cmd.Err() +} diff --git a/pkg/infra/remotecache/redis_storage_integration_test.go b/pkg/infra/remotecache/redis_storage_integration_test.go new file mode 100644 index 00000000000..8d54fc9ff14 --- /dev/null +++ b/pkg/infra/remotecache/redis_storage_integration_test.go @@ -0,0 +1,16 @@ +// +build redis + +package remotecache + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" +) + +func TestRedisCacheStorage(t *testing.T) { + + opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: "localhost:6379"} + client := createTestClient(t, opts, nil) + runTestsForClient(t, client) +} diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go new file mode 100644 index 00000000000..9219fa33a08 --- /dev/null +++ b/pkg/infra/remotecache/remotecache.go @@ -0,0 +1,133 @@ +package remotecache + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "time" + + "github.com/grafana/grafana/pkg/setting" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + + "github.com/grafana/grafana/pkg/registry" +) + +var ( + // ErrCacheItemNotFound is returned if cache does not exist + ErrCacheItemNotFound = errors.New("cache item not found") + + // ErrInvalidCacheType is returned if the type is invalid + ErrInvalidCacheType = errors.New("invalid remote cache name") + + defaultMaxCacheExpiration = time.Hour * 24 +) + +func init() { + registry.RegisterService(&RemoteCache{}) +} + +// CacheStorage allows the caller to set, get and delete items in the cache. +// Cached items are stored as byte arrays and marshalled using "encoding/gob" +// so any struct added to the cache needs to be registred with `remotecache.Register` +// ex `remotecache.Register(CacheableStruct{})`` +type CacheStorage interface { + // Get reads object from Cache + Get(key string) (interface{}, error) + + // Set sets an object into the cache. if `expire` is set to zero it will default to 24h + Set(key string, value interface{}, expire time.Duration) error + + // Delete object from cache + Delete(key string) error +} + +// RemoteCache allows Grafana to cache data outside its own process +type RemoteCache struct { + log log.Logger + client CacheStorage + SQLStore *sqlstore.SqlStore `inject:""` + Cfg *setting.Cfg `inject:""` +} + +// Get reads object from Cache +func (ds *RemoteCache) Get(key string) (interface{}, error) { + return ds.client.Get(key) +} + +// Set sets an object into the cache. if `expire` is set to zero it will default to 24h +func (ds *RemoteCache) Set(key string, value interface{}, expire time.Duration) error { + if expire == 0 { + expire = defaultMaxCacheExpiration + } + + return ds.client.Set(key, value, expire) +} + +// Delete object from cache +func (ds *RemoteCache) Delete(key string) error { + return ds.client.Delete(key) +} + +// Init initializes the service +func (ds *RemoteCache) Init() error { + ds.log = log.New("cache.remote") + var err error + ds.client, err = createClient(ds.Cfg.RemoteCacheOptions, ds.SQLStore) + return err +} + +// Run start the backend processes for cache clients +func (ds *RemoteCache) Run(ctx context.Context) error { + //create new interface if more clients need GC jobs + backgroundjob, ok := ds.client.(registry.BackgroundService) + if ok { + return backgroundjob.Run(ctx) + } + + <-ctx.Done() + return ctx.Err() +} + +func createClient(opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) (CacheStorage, error) { + if opts.Name == redisCacheType { + return newRedisStorage(opts), nil + } + + if opts.Name == memcachedCacheType { + return newMemcachedStorage(opts), nil + } + + if opts.Name == databaseCacheType { + return newDatabaseCache(sqlstore), nil + } + + return nil, ErrInvalidCacheType +} + +// Register records a type, identified by a value for that type, under its +// internal type name. That name will identify the concrete type of a value +// sent or received as an interface variable. Only types that will be +// transferred as implementations of interface values need to be registered. +// Expecting to be used only during initialization, it panics if the mapping +// between types and names is not a bijection. +func Register(value interface{}) { + gob.Register(value) +} + +type cachedItem struct { + Val interface{} +} + +func encodeGob(item *cachedItem) ([]byte, error) { + buf := bytes.NewBuffer(nil) + err := gob.NewEncoder(buf).Encode(item) + return buf.Bytes(), err +} + +func decodeGob(data []byte, out *cachedItem) error { + buf := bytes.NewBuffer(data) + return gob.NewDecoder(buf).Decode(&out) +} diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go new file mode 100644 index 00000000000..bf1675ec87c --- /dev/null +++ b/pkg/infra/remotecache/remotecache_test.go @@ -0,0 +1,93 @@ +package remotecache + +import ( + "testing" + "time" + + "github.com/bmizerany/assert" + + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +type CacheableStruct struct { + String string + Int64 int64 +} + +func init() { + Register(CacheableStruct{}) +} + +func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) CacheStorage { + t.Helper() + + dc := &RemoteCache{ + SQLStore: sqlstore, + Cfg: &setting.Cfg{ + RemoteCacheOptions: opts, + }, + } + + err := dc.Init() + if err != nil { + t.Fatalf("failed to init client for test. error: %v", err) + } + + return dc +} + +func TestCachedBasedOnConfig(t *testing.T) { + + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ + HomePath: "../../../", + }) + + client := createTestClient(t, cfg.RemoteCacheOptions, sqlstore.InitTestDB(t)) + runTestsForClient(t, client) +} + +func TestInvalidCacheTypeReturnsError(t *testing.T) { + _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil) + assert.Equal(t, err, ErrInvalidCacheType) +} + +func runTestsForClient(t *testing.T, client CacheStorage) { + canPutGetAndDeleteCachedObjects(t, client) + canNotFetchExpiredItems(t, client) +} + +func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + err := client.Set("key1", cacheableStruct, 0) + assert.Equal(t, err, nil, "expected nil. got: ", err) + + data, err := client.Get("key1") + 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("key1") + assert.Equal(t, err, nil) + + _, err = client.Get("key1") + assert.Equal(t, err, ErrCacheItemNotFound) +} + +func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + err := client.Set("key1", cacheableStruct, time.Second) + assert.Equal(t, err, nil) + + //not sure how this can be avoided when testing redis/memcached :/ + <-time.After(time.Second + time.Millisecond) + + // should not be able to read that value since its expired + _, err = client.Get("key1") + assert.Equal(t, err, ErrCacheItemNotFound) +} 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..3467b88962b --- /dev/null +++ b/pkg/services/sqlstore/migrations/cache_data_mig.go @@ -0,0 +1,22 @@ +package migrations + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addCacheMigration(mg *migrator.Migrator) { + var cacheDataV1 = migrator.Table{ + Name: "cache_data", + Columns: []*migrator.Column{ + {Name: "cache_key", Type: migrator.DB_NVarchar, IsPrimaryKey: true, Length: 168}, + {Name: "data", Type: migrator.DB_Blob}, + {Name: "expires", Type: migrator.DB_Integer, Length: 255, Nullable: false}, + {Name: "created_at", Type: migrator.DB_Integer, Length: 255, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"cache_key"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create cache_data table", migrator.NewAddTableMigration(cacheDataV1)) + + mg.AddMigration("add unique index cache_data.cache_key", migrator.NewAddIndexMigration(cacheDataV1, cacheDataV1.Indices[0])) +} 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) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 7b6e99255aa..ac16cc73e9c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -241,6 +241,9 @@ type Cfg struct { // User EditorsCanOwn bool + + // DistributedCache + RemoteCacheOptions *RemoteCacheOptions } type CommandLineArgs struct { @@ -781,9 +784,20 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { enterprise := iniFile.Section("enterprise") cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt")) + cacheServer := iniFile.Section("remote_cache") + cfg.RemoteCacheOptions = &RemoteCacheOptions{ + Name: cacheServer.Key("type").MustString("database"), + ConnStr: cacheServer.Key("connstr").MustString(""), + } + return nil } +type RemoteCacheOptions struct { + Name string + ConnStr string +} + func (cfg *Cfg) readSessionConfig() { sec := cfg.Raw.Section("session") SessionOptions = session.Options{} diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh new file mode 100755 index 00000000000..bacd9928362 --- /dev/null +++ b/scripts/circle-test-cache-servers.sh @@ -0,0 +1,16 @@ +#!/bin/bash +function exit_if_fail { + command=$@ + echo "Executing '$command'" + eval $command + rc=$? + if [ $rc -ne 0 ]; then + echo "'$command' returned $rc." + exit $rc + fi +} + +echo "running redis and memcache tests" + +time exit_if_fail go test -tags=redis ./pkg/infra/remotecache/... +time exit_if_fail go test -tags=memcached ./pkg/infra/remotecache/...