diff --git a/go.mod b/go.mod index afeb79e..8a34d06 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,17 @@ module github.com/kumparan/cacher/v2 require ( + github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect + github.com/alicebob/miniredis v2.5.0+incompatible github.com/bsm/redislock v0.4.3 github.com/go-redis/redis v6.15.6+incompatible + github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 github.com/kr/pretty v0.1.0 // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/onsi/gomega v1.8.1 // indirect + github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect - golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index a1b0599..bf6c723 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,17 @@ +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= github.com/bsm/redislock v0.4.3 h1:TJ0RzHeSujLSuy4b33OWDknxAzKCdLdit0Hs9kOjElg= github.com/bsm/redislock v0.4.3/go.mod h1:mcygIsJknQThqWrlOgiPJ97CGmu3aAdQabg1ZIxT1BA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= @@ -22,6 +28,8 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/keeper.go b/keeper.go index 0117374..4eb6825 100644 --- a/keeper.go +++ b/keeper.go @@ -276,48 +276,6 @@ func (k *keeper) StoreNil(cacheKey string) error { return err } -// Purge :nodoc: -// func (k *keeper) Purge(matchString string) error { -// if k.disableCaching { -// return nil -// } - -// client := k.connPool.Get() -// - -// var cursor interface{} -// var stop []uint8 -// cursor = "0" -// delCount := 0 -// for { -// res, err := redigo.Values(client.Do("SCAN", cursor, "MATCH", matchString, "COUNT", 500000)) -// if err != nil { -// return err -// } -// stop = res[0].([]uint8) -// if foundKeys, ok := res[1].([]interface{}); ok { -// if len(foundKeys) > 0 { -// err = client.Send("DEL", foundKeys...) -// if err != nil { -// return err -// } -// delCount++ -// } - -// // ascii for '0' is 48 -// if stop[0] == 48 { -// break -// } -// } - -// cursor = res[0] -// } -// if delCount > 0 { -// client.Flush() -// } -// return nil -// } - // IncreaseCachedValueByOne will increments the number stored at key by one. // If the key does not exist, it is set to 0 before performing the operation func (k *keeper) IncreaseCachedValueByOne(key string) error { diff --git a/keeper_test.go b/keeper_test.go new file mode 100644 index 0000000..a359c97 --- /dev/null +++ b/keeper_test.go @@ -0,0 +1,497 @@ +package cacher + +import ( + "os" + "testing" + "time" + + "github.com/alicebob/miniredis" + "github.com/go-redis/redis" +) + +var client *redis.Client + +func newTestKeeper() *keeper { + return &keeper{ + connPool: client, + lockConnPool: client, + disableCaching: false, + lockDuration: defaultLockDuration, + defaultTTL: defaultTTL, + nilTTL: defaultNilTTL, + waitTime: defaultWaitTime, + } +} + +func TestMain(m *testing.M) { + mr, err := miniredis.Run() + if err != nil { + panic(err) + } + + client = redis.NewClient(&redis.Options{ + Addr: mr.Addr(), + }) + + i := m.Run() + mr.Close() + + os.Exit(i) +} + +func TestKeeper_AcquireLock(t *testing.T) { + keeper := newTestKeeper() + key := "test" + lock, err := keeper.AcquireLock(key) + if err != nil { + t.Fatal(err) + } + + if lock == nil { + t.Fatal("lock shouldn't be nil") + } + + lock2, err := keeper.AcquireLock(key) + if err == nil { + t.Fatal("should be error lock not obtained") + } + + if lock2 != nil { + t.Fatal("lock 2 should be nil") + } + + err = lock.Release() + if err != nil { + t.Fatal(err) + } +} + +func TestKeeper_Get(t *testing.T) { + keeper := newTestKeeper() + key := "test" + res, err := keeper.Get(key) + if err != nil { + t.Fatal(err) + } + + if res != nil { + t.Fatal("result should be nil") + } + + cmd := client.Set(key, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + res, err = keeper.Get(key) + if err != nil { + t.Fatal(err) + } + + if res == nil { + t.Fatal("result should not be nil") + } +} + +func TestKeeper_GetOrLock(t *testing.T) { + t.Run("getting the lock", func(t *testing.T) { + keeper := newTestKeeper() + key := "test-get-or-lock" + + res, lock, err := keeper.GetOrLock(key) + if err != nil { + t.Fatal(err) + } + if lock == nil { + t.Fatal("should getting the lock") + } + if res != nil { + t.Fatal("result should be nil") + } + }) + + t.Run("wait to getting lock", func(t *testing.T) { + keeper := newTestKeeper() + key := "test-get-or-lock" + + cmd := client.Set("lock:"+key, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + doneCh := make(chan struct{}) + go func() { + defer close(doneCh) + res, lock, err := keeper.GetOrLock(key) + if err != nil { + t.Fatal(err) + } + if lock != nil { + t.Fatal("should not getting the lock") + } + if res == nil { + t.Fatal("result should not be nil") + } + b, ok := res.([]byte) + if !ok { + t.Fatal("failed to casting to bytes") + } + if string(b) != "test" { + t.Fatal("invalid value") + } + }() + + cmd = client.Set(key, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + dumpCmd := client.Del("lock:" + key) + if dumpCmd.Err() != nil { + t.Fatal(dumpCmd.Err()) + } + + <-doneCh + }) +} + +func TestKeeper_GetOrSet(t *testing.T) { + keeper := newTestKeeper() + key := "test-get-or-set" + + res, err := keeper.GetOrSet(key, func() (interface{}, error) { + return []byte("test"), nil + }, defaultTTL) + if err != nil { + t.Fatal(err) + } + if res == nil { + t.Fatal("result should not be nil") + } + + // second call, getting data from cache + res, err = keeper.GetOrSet(key, nil, defaultTTL) + if err != nil { + t.Fatal(err) + } + if res == nil { + t.Fatal("result should not be nil") + } +} + +func TestKeeper_CheckKeyExist(t *testing.T) { + keeper := newTestKeeper() + key := "test-key-exists" + + exists, err := keeper.CheckKeyExist(key) + if err != nil { + t.Fatal(err) + } + + if exists { + t.Fatal("should not exists") + } + + cmd := client.Set(key, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + exists, err = keeper.CheckKeyExist(key) + if err != nil { + t.Fatal(err) + } + + if !exists { + t.Fatal("should exists") + } +} + +func TestKeeper_DeleteByKeys(t *testing.T) { + keeper := newTestKeeper() + key1 := "test-delete-keys-1" + cmd := client.Set(key1, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + key2 := "test-delete-keys-2" + cmd = client.Set(key2, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + err := keeper.DeleteByKeys([]string{key1, key2}) + if err != nil { + t.Fatal(err) + } + + exCmd := client.Exists(key1, key2) + if exCmd.Err() != nil { + t.Fatal(exCmd.Err()) + } + if exCmd.Val() != 0 { + t.Fatal("should 0") + } +} + +func TestKeeper_GetHashMember(t *testing.T) { + keeper := newTestKeeper() + + bucket := "test-bucket" + key := "test-get-hash-member" + + res, err := keeper.GetHashMember(bucket, key) + if err != nil { + t.Fatal(err) + } + if res != nil { + t.Fatal("should be nil") + } + + bCmd := client.HSet(bucket, key, []byte("test")) + if bCmd.Err() != nil { + t.Fatal(bCmd) + } + + res, err = keeper.GetHashMember(bucket, key) + if err != nil { + t.Fatal(err) + } + if res == nil { + t.Fatal("should not be nil") + } +} + +func TestKeeper_GetHashMemberOrLock(t *testing.T) { + t.Run("getting the lock", func(t *testing.T) { + keeper := newTestKeeper() + bucket := "test-bucket-hash" + key := "test-get-or-lock-hash" + + res, lock, err := keeper.GetHashMemberOrLock(bucket, key) + if err != nil { + t.Fatal(err) + } + if lock == nil { + t.Fatal("should getting the lock") + } + if res != nil { + t.Fatal("result should be nil") + } + + err = lock.Release() + if err != nil { + t.Fatal(err) + } + }) + + t.Run("wait to getting lock", func(t *testing.T) { + keeper := newTestKeeper() + bucket := "test-bucket" + key := "test-get-or-lock" + lock := "lock:" + bucket + ":" + key + + cmd := client.Set(lock, []byte("test"), 500*time.Millisecond) + if cmd.Err() != nil { + t.Fatal(cmd.Err()) + } + + doneCh := make(chan struct{}) + go func() { + defer close(doneCh) + res, lock, err := keeper.GetHashMemberOrLock(bucket, key) + if err != nil { + t.Fatal(err) + } + if lock != nil { + t.Fatal("should not getting the lock") + } + if res == nil { + t.Fatal("result should not be nil") + } + b, ok := res.([]byte) + if !ok { + t.Fatal("failed to casting to bytes") + } + if string(b) != "test" { + t.Fatal("invalid value") + } + }() + + hsetCmd := client.HSet(bucket, key, []byte("test")) + if hsetCmd.Err() != nil { + t.Fatal(hsetCmd.Err()) + } + + dumpCmd := client.Del(lock) + if dumpCmd.Err() != nil { + t.Fatal(dumpCmd.Err()) + } + + <-doneCh + }) +} + +func TestKeeper_DeleteHashMember(t *testing.T) { + keeper := newTestKeeper() + bucket := "test-bucket" + key1 := "test-get-or-lock" + key2 := "test-get-or-lock-2" + + hsetCmd := client.HSet(bucket, key1, []byte("test")) + if hsetCmd.Err() != nil { + t.Fatal(hsetCmd.Err()) + } + + hsetCmd = client.HSet(bucket, key2, []byte("test2")) + if hsetCmd.Err() != nil { + t.Fatal(hsetCmd.Err()) + } + + err := keeper.DeleteHashMember(bucket, key1) + if err != nil { + t.Fatal(err) + } + + err = keeper.DeleteHashMember(bucket, key2) + if err != nil { + t.Fatal(err) + } + + bCmd := client.Exists(bucket) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 0 { + t.Fatal("should be 0") + } +} + +func TestKeeper_Store(t *testing.T) { + keeper := newTestKeeper() + key := "test-store" + + bCmd := client.Exists(key) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 0 { + t.Fatal("should be 0") + } + + lock, err := keeper.AcquireLock(key) + if err != nil { + t.Fatal(err) + } + + err = keeper.Store(lock, NewItem(key, []byte("test"))) + if err != nil { + t.Fatal(err) + } + + bCmd = client.Exists(key) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 1 { + t.Fatal("should be 1") + } +} + +func TestKeeper_StoreWithoutBlocking(t *testing.T) { + keeper := newTestKeeper() + key := "test-store-without-blocking" + + bCmd := client.Exists(key) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 0 { + t.Fatal("should be 0") + } + + err := keeper.StoreWithoutBlocking(NewItem(key, []byte("test"))) + if err != nil { + t.Fatal(err) + } + + bCmd = client.Exists(key) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 1 { + t.Fatal("should be 1") + } +} + +func TestKeeper_StoreHashMember(t *testing.T) { + keeper := newTestKeeper() + bucket := "test-bucket-store" + key := "test-store-without-blocking" + + bCmd := client.Exists(bucket) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 0 { + t.Fatal("should be 0") + } + + err := keeper.StoreHashMember(bucket, NewItem(key, []byte("test"))) + if err != nil { + t.Fatal(err) + } + + bCmd = client.Exists(bucket) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 1 { + t.Fatal("should be 1") + } +} + +func TestKeeper_StoreMultiWithoutBlocking(t *testing.T) { + keeper := newTestKeeper() + item1 := &item{ + key: "key-1", + ttl: defaultTTL, + value: []byte("test-1"), + } + item2 := &item{ + key: "key-2", + ttl: defaultTTL, + value: []byte("test-2"), + } + + bCmd := client.Exists(item1.key, item2.key) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 0 { + t.Fatal("should be 0") + } + + err := keeper.StoreMultiWithoutBlocking([]Item{item1, item2}) + if err != nil { + t.Fatal(err) + } + + bCmd = client.Exists(item1.key, item2.key) + if bCmd.Err() != nil { + t.Fatal(bCmd.Err()) + } + + if bCmd.Val() != 2 { + t.Fatal("should be 1") + } +}