|
|
|
package sqlc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"runtime"
|
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/zeromicro/go-zero/core/fx"
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
|
|
"github.com/zeromicro/go-zero/core/stat"
|
|
|
|
"github.com/zeromicro/go-zero/core/stores/cache"
|
|
|
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
|
|
|
"github.com/zeromicro/go-zero/core/stores/redis/redistest"
|
|
|
|
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
|
|
|
"github.com/zeromicro/go-zero/core/syncx"
|
|
|
|
"github.com/zeromicro/go-zero/internal/dbtest"
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
logx.Disable()
|
|
|
|
stat.SetReporter(nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_GetCache(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
|
|
|
var value string
|
|
|
|
err := c.GetCache("any", &value)
|
|
|
|
assert.Equal(t, ErrNotFound, err)
|
|
|
|
_ = r.Set("any", `"value"`)
|
|
|
|
err = c.GetCache("any", &value)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, "value", value)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStat(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
var str string
|
|
|
|
err := c.QueryRow(&str, "name", func(conn sqlx.SqlConn, v any) error {
|
|
|
|
*v.(*string) = "zero"
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.Equal(t, uint64(10), atomic.LoadUint64(&stats.Total))
|
|
|
|
assert.Equal(t, uint64(9), atomic.LoadUint64(&stats.Hit))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_QueryRowIndex_NoCache(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewConn(dummySqlConn{}, cache.CacheConf{
|
|
|
|
{
|
|
|
|
RedisConf: redis.RedisConf{
|
|
|
|
Host: r.Addr,
|
|
|
|
Type: redis.NodeType,
|
|
|
|
},
|
|
|
|
Weight: 100,
|
|
|
|
},
|
|
|
|
}, cache.WithExpiry(time.Second*10))
|
|
|
|
|
|
|
|
var str string
|
|
|
|
err := c.QueryRowIndex(&str, "index", func(s any) string {
|
|
|
|
return fmt.Sprintf("%s/1234", s)
|
|
|
|
}, func(conn sqlx.SqlConn, v any) (any, error) {
|
|
|
|
*v.(*string) = "zero"
|
|
|
|
return "primary", errors.New("foo")
|
|
|
|
}, func(conn sqlx.SqlConn, v, pri any) error {
|
|
|
|
assert.Equal(t, "primary", pri)
|
|
|
|
*v.(*string) = "xin"
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
|
|
|
err = c.QueryRowIndex(&str, "index", func(s any) string {
|
|
|
|
return fmt.Sprintf("%s/1234", s)
|
|
|
|
}, func(conn sqlx.SqlConn, v any) (any, error) {
|
|
|
|
*v.(*string) = "zero"
|
|
|
|
return "primary", nil
|
|
|
|
}, func(conn sqlx.SqlConn, v, pri any) error {
|
|
|
|
assert.Equal(t, "primary", pri)
|
|
|
|
*v.(*string) = "xin"
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, "zero", str)
|
|
|
|
val, err := r.Get("index")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"primary"`, val)
|
|
|
|
val, err = r.Get("primary/1234")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"zero"`, val)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_QueryRowIndex_HasCache(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
|
|
|
|
cache.WithNotFoundExpiry(time.Second))
|
|
|
|
|
|
|
|
var str string
|
|
|
|
r.Set("index", `"primary"`)
|
|
|
|
err := c.QueryRowIndex(&str, "index", func(s any) string {
|
|
|
|
return fmt.Sprintf("%s/1234", s)
|
|
|
|
}, func(conn sqlx.SqlConn, v any) (any, error) {
|
|
|
|
assert.Fail(t, "should not go here")
|
|
|
|
return "primary", nil
|
|
|
|
}, func(conn sqlx.SqlConn, v, primary any) error {
|
|
|
|
*v.(*string) = "xin"
|
|
|
|
assert.Equal(t, "primary", primary)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, "xin", str)
|
|
|
|
val, err := r.Get("index")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"primary"`, val)
|
|
|
|
val, err = r.Get("primary/1234")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"xin"`, val)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_QueryRowIndex_HasCache_IntPrimary(t *testing.T) {
|
|
|
|
const (
|
|
|
|
primaryInt8 int8 = 100
|
|
|
|
primaryInt16 int16 = 10000
|
|
|
|
primaryInt32 int32 = 10000000
|
|
|
|
primaryInt64 int64 = 10000000
|
|
|
|
primaryUint8 uint8 = 100
|
|
|
|
primaryUint16 uint16 = 10000
|
|
|
|
primaryUint32 uint32 = 10000000
|
|
|
|
primaryUint64 uint64 = 10000000
|
|
|
|
)
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
primary any
|
|
|
|
primaryCache string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "int8 primary",
|
|
|
|
primary: primaryInt8,
|
|
|
|
primaryCache: fmt.Sprint(primaryInt8),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "int16 primary",
|
|
|
|
primary: primaryInt16,
|
|
|
|
primaryCache: fmt.Sprint(primaryInt16),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "int32 primary",
|
|
|
|
primary: primaryInt32,
|
|
|
|
primaryCache: fmt.Sprint(primaryInt32),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "int64 primary",
|
|
|
|
primary: primaryInt64,
|
|
|
|
primaryCache: fmt.Sprint(primaryInt64),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "uint8 primary",
|
|
|
|
primary: primaryUint8,
|
|
|
|
primaryCache: fmt.Sprint(primaryUint8),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "uint16 primary",
|
|
|
|
primary: primaryUint16,
|
|
|
|
primaryCache: fmt.Sprint(primaryUint16),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "uint32 primary",
|
|
|
|
primary: primaryUint32,
|
|
|
|
primaryCache: fmt.Sprint(primaryUint32),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "uint64 primary",
|
|
|
|
primary: primaryUint64,
|
|
|
|
primaryCache: fmt.Sprint(primaryUint64),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
|
|
|
|
cache.WithNotFoundExpiry(time.Second))
|
|
|
|
|
|
|
|
var str string
|
|
|
|
r.Set("index", test.primaryCache)
|
|
|
|
err := c.QueryRowIndex(&str, "index", func(s any) string {
|
|
|
|
return fmt.Sprintf("%v/1234", s)
|
|
|
|
}, func(conn sqlx.SqlConn, v any) (any, error) {
|
|
|
|
assert.Fail(t, "should not go here")
|
|
|
|
return test.primary, nil
|
|
|
|
}, func(conn sqlx.SqlConn, v, primary any) error {
|
|
|
|
*v.(*string) = "xin"
|
|
|
|
assert.Equal(t, primary, primary)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, "xin", str)
|
|
|
|
val, err := r.Get("index")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, test.primaryCache, val)
|
|
|
|
val, err = r.Get(test.primaryCache + "/1234")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"xin"`, val)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_QueryRowIndex_HasWrongCache(t *testing.T) {
|
|
|
|
caches := map[string]string{
|
|
|
|
"index": "primary",
|
|
|
|
"primary/1234": "xin",
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range caches {
|
|
|
|
t.Run(k+"/"+v, func(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
|
|
|
|
cache.WithNotFoundExpiry(time.Second))
|
|
|
|
|
|
|
|
var str string
|
|
|
|
r.Set(k, v)
|
|
|
|
err := c.QueryRowIndex(&str, "index", func(s any) string {
|
|
|
|
return fmt.Sprintf("%s/1234", s)
|
|
|
|
}, func(conn sqlx.SqlConn, v any) (any, error) {
|
|
|
|
*v.(*string) = "xin"
|
|
|
|
return "primary", nil
|
|
|
|
}, func(conn sqlx.SqlConn, v, primary any) error {
|
|
|
|
*v.(*string) = "xin"
|
|
|
|
assert.Equal(t, "primary", primary)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, "xin", str)
|
|
|
|
val, err := r.Get("index")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"primary"`, val)
|
|
|
|
val, err = r.Get("primary/1234")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, `"xin"`, val)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStatCacheFails(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
log.SetOutput(io.Discard)
|
|
|
|
defer log.SetOutput(os.Stdout)
|
|
|
|
|
|
|
|
r := redis.New("localhost:59999")
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
|
|
|
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
var str string
|
|
|
|
err := c.QueryRow(&str, "name", func(conn sqlx.SqlConn, v any) error {
|
|
|
|
return errors.New("db failed")
|
|
|
|
})
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.Equal(t, uint64(20), atomic.LoadUint64(&stats.Total))
|
|
|
|
assert.Equal(t, uint64(0), atomic.LoadUint64(&stats.Hit))
|
|
|
|
assert.Equal(t, uint64(20), atomic.LoadUint64(&stats.Miss))
|
|
|
|
assert.Equal(t, uint64(0), atomic.LoadUint64(&stats.DbFails))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStatDbFails(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
|
|
|
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
var str string
|
|
|
|
err := c.QueryRow(&str, "name", func(conn sqlx.SqlConn, v any) error {
|
|
|
|
return errors.New("db failed")
|
|
|
|
})
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.Equal(t, uint64(20), atomic.LoadUint64(&stats.Total))
|
|
|
|
assert.Equal(t, uint64(0), atomic.LoadUint64(&stats.Hit))
|
|
|
|
assert.Equal(t, uint64(20), atomic.LoadUint64(&stats.DbFails))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStatFromMemory(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
|
|
|
|
|
|
|
var all sync.WaitGroup
|
|
|
|
var wait sync.WaitGroup
|
|
|
|
all.Add(10)
|
|
|
|
wait.Add(4)
|
|
|
|
go func() {
|
|
|
|
var str string
|
|
|
|
err := c.QueryRow(&str, "name", func(conn sqlx.SqlConn, v any) error {
|
|
|
|
*v.(*string) = "zero"
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
wait.Wait()
|
|
|
|
runtime.Gosched()
|
|
|
|
all.Done()
|
|
|
|
}()
|
|
|
|
|
|
|
|
for i := 0; i < 4; i++ {
|
|
|
|
go func() {
|
|
|
|
var str string
|
|
|
|
wait.Done()
|
|
|
|
err := c.QueryRow(&str, "name", func(conn sqlx.SqlConn, v any) error {
|
|
|
|
*v.(*string) = "zero"
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
all.Done()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
for i := 0; i < 5; i++ {
|
|
|
|
go func() {
|
|
|
|
var str string
|
|
|
|
err := c.QueryRow(&str, "name", func(conn sqlx.SqlConn, v any) error {
|
|
|
|
*v.(*string) = "zero"
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
all.Done()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
all.Wait()
|
|
|
|
|
|
|
|
assert.Equal(t, uint64(10), atomic.LoadUint64(&stats.Total))
|
|
|
|
assert.Equal(t, uint64(9), atomic.LoadUint64(&stats.Hit))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_DelCache(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
const (
|
|
|
|
key = "user"
|
|
|
|
value = "any"
|
|
|
|
)
|
|
|
|
assert.NoError(t, r.Set(key, value))
|
|
|
|
|
|
|
|
c := NewNodeConn(&trackedConn{}, r, cache.WithExpiry(time.Second*30))
|
|
|
|
err := c.DelCache(key)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
|
|
|
|
val, err := r.Get(key)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Empty(t, val)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnQueryRow(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
const (
|
|
|
|
key = "user"
|
|
|
|
value = "any"
|
|
|
|
)
|
|
|
|
var conn trackedConn
|
|
|
|
var user string
|
|
|
|
var ran bool
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
|
|
|
err := c.QueryRow(&user, key, func(conn sqlx.SqlConn, v any) error {
|
|
|
|
ran = true
|
|
|
|
user = value
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
actualValue, err := r.Get(key)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
var actual string
|
|
|
|
assert.Nil(t, json.Unmarshal([]byte(actualValue), &actual))
|
|
|
|
assert.Equal(t, value, actual)
|
|
|
|
assert.Equal(t, value, user)
|
|
|
|
assert.True(t, ran)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnQueryRowFromCache(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
const (
|
|
|
|
key = "user"
|
|
|
|
value = "any"
|
|
|
|
)
|
|
|
|
var conn trackedConn
|
|
|
|
var user string
|
|
|
|
var ran bool
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
|
|
|
assert.Nil(t, c.SetCache(key, value))
|
|
|
|
err := c.QueryRow(&user, key, func(conn sqlx.SqlConn, v any) error {
|
|
|
|
ran = true
|
|
|
|
user = value
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
actualValue, err := r.Get(key)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
var actual string
|
|
|
|
assert.Nil(t, json.Unmarshal([]byte(actualValue), &actual))
|
|
|
|
assert.Equal(t, value, actual)
|
|
|
|
assert.Equal(t, value, user)
|
|
|
|
assert.False(t, ran)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestQueryRowNotFound(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
const key = "user"
|
|
|
|
var conn trackedConn
|
|
|
|
var user string
|
|
|
|
var ran int
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
err := c.QueryRow(&user, key, func(conn sqlx.SqlConn, v any) error {
|
|
|
|
ran++
|
|
|
|
return sql.ErrNoRows
|
|
|
|
})
|
|
|
|
assert.Exactly(t, sqlx.ErrNotFound, err)
|
|
|
|
}
|
|
|
|
assert.Equal(t, 1, ran)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnExec(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
var conn trackedConn
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
|
|
|
_, err := c.ExecNoCache("delete from user_table where id='kevin'")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.True(t, conn.execValue)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnExecDropCache(t *testing.T) {
|
|
|
|
t.Run("drop cache", func(t *testing.T) {
|
|
|
|
r, err := miniredis.Run()
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer fx.DoWithTimeout(func() error {
|
|
|
|
r.Close()
|
|
|
|
return nil
|
|
|
|
}, time.Second)
|
|
|
|
|
|
|
|
const (
|
|
|
|
key = "user"
|
|
|
|
value = "any"
|
|
|
|
)
|
|
|
|
var conn trackedConn
|
|
|
|
c := NewNodeConn(&conn, redis.New(r.Addr()), cache.WithExpiry(time.Second*30))
|
|
|
|
assert.Nil(t, c.SetCache(key, value))
|
|
|
|
_, err = c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
|
|
|
|
return conn.Exec("delete from user_table where id='kevin'")
|
|
|
|
}, key)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.True(t, conn.execValue)
|
|
|
|
_, err = r.Get(key)
|
|
|
|
assert.Exactly(t, miniredis.ErrKeyNotFound, err)
|
|
|
|
_, err = c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
|
|
|
|
return nil, errors.New("foo")
|
|
|
|
}, key)
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_SetCacheWithExpire(t *testing.T) {
|
|
|
|
r, err := miniredis.Run()
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer fx.DoWithTimeout(func() error {
|
|
|
|
r.Close()
|
|
|
|
return nil
|
|
|
|
}, time.Second)
|
|
|
|
|
|
|
|
const (
|
|
|
|
key = "user"
|
|
|
|
value = "any"
|
|
|
|
)
|
|
|
|
var conn trackedConn
|
|
|
|
c := NewNodeConn(&conn, redis.New(r.Addr()), cache.WithExpiry(time.Second*30))
|
|
|
|
assert.Nil(t, c.SetCacheWithExpire(key, value, time.Minute))
|
|
|
|
val, err := r.Get(key)
|
|
|
|
if assert.NoError(t, err) {
|
|
|
|
ttl := r.TTL(key)
|
|
|
|
assert.True(t, ttl > 0 && ttl <= time.Minute)
|
|
|
|
assert.Equal(t, fmt.Sprintf("%q", value), val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnExecDropCacheFailed(t *testing.T) {
|
|
|
|
const key = "user"
|
|
|
|
var conn trackedConn
|
|
|
|
r := redis.New("anyredis:8888")
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
|
|
|
_, err := c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
|
|
|
|
return conn.Exec("delete from user_table where id='kevin'")
|
|
|
|
}, key)
|
|
|
|
// async background clean, retry logic
|
|
|
|
assert.Nil(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnQueryRows(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
var conn trackedConn
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
|
|
|
var users []string
|
|
|
|
err := c.QueryRowsNoCache(&users, "select user from user_table where id='kevin'")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.True(t, conn.queryRowsValue)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConnTransact(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
var conn trackedConn
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
|
|
|
err := c.Transact(func(session sqlx.Session) error {
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.True(t, conn.transactValue)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestQueryRowNoCache(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
const (
|
|
|
|
key = "user"
|
|
|
|
value = "any"
|
|
|
|
)
|
|
|
|
var user string
|
|
|
|
var ran bool
|
|
|
|
conn := dummySqlConn{queryRow: func(v any, q string, args ...any) error {
|
|
|
|
user = value
|
|
|
|
ran = true
|
|
|
|
return nil
|
|
|
|
}}
|
|
|
|
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
|
|
|
err := c.QueryRowNoCache(&user, key)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, value, user)
|
|
|
|
assert.True(t, ran)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNewConnWithCache(t *testing.T) {
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
|
|
|
|
var conn trackedConn
|
|
|
|
c := NewConnWithCache(&conn, cache.NewNode(r, singleFlights, stats, sql.ErrNoRows))
|
|
|
|
_, err := c.ExecNoCache("delete from user_table where id='kevin'")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
assert.True(t, conn.execValue)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCachedConn_WithSession(t *testing.T) {
|
|
|
|
dbtest.RunTxTest(t, func(tx *sql.Tx, mock sqlmock.Sqlmock) {
|
|
|
|
mock.ExpectExec("any").WillReturnResult(sqlmock.NewResult(2, 3))
|
|
|
|
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
conn := CachedConn{
|
|
|
|
cache: cache.NewNode(r, syncx.NewSingleFlight(), stats, sql.ErrNoRows),
|
|
|
|
}
|
|
|
|
conn = conn.WithSession(sqlx.NewSessionFromTx(tx))
|
|
|
|
res, err := conn.Exec(func(conn sqlx.SqlConn) (sql.Result, error) {
|
|
|
|
return conn.Exec("any")
|
|
|
|
}, "foo")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
last, err := res.LastInsertId()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(2), last)
|
|
|
|
affected, err := res.RowsAffected()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(3), affected)
|
|
|
|
})
|
|
|
|
|
|
|
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
|
|
|
mock.ExpectBegin()
|
|
|
|
mock.ExpectExec("any").WillReturnResult(sqlmock.NewResult(2, 3))
|
|
|
|
mock.ExpectCommit()
|
|
|
|
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
conn := CachedConn{
|
|
|
|
db: sqlx.NewSqlConnFromDB(db),
|
|
|
|
cache: cache.NewNode(r, syncx.NewSingleFlight(), stats, sql.ErrNoRows),
|
|
|
|
}
|
|
|
|
assert.NoError(t, conn.Transact(func(session sqlx.Session) error {
|
|
|
|
conn = conn.WithSession(session)
|
|
|
|
res, err := conn.Exec(func(conn sqlx.SqlConn) (sql.Result, error) {
|
|
|
|
return conn.Exec("any")
|
|
|
|
}, "foo")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
last, err := res.LastInsertId()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(2), last)
|
|
|
|
affected, err := res.RowsAffected()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(3), affected)
|
|
|
|
return nil
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
|
|
|
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
|
|
|
mock.ExpectBegin()
|
|
|
|
mock.ExpectExec("any").WillReturnError(errors.New("foo"))
|
|
|
|
mock.ExpectRollback()
|
|
|
|
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
conn := CachedConn{
|
|
|
|
db: sqlx.NewSqlConnFromDB(db),
|
|
|
|
cache: cache.NewNode(r, syncx.NewSingleFlight(), stats, sql.ErrNoRows),
|
|
|
|
}
|
|
|
|
assert.Error(t, conn.Transact(func(session sqlx.Session) error {
|
|
|
|
conn = conn.WithSession(session)
|
|
|
|
_, err := conn.Exec(func(conn sqlx.SqlConn) (sql.Result, error) {
|
|
|
|
return conn.Exec("any")
|
|
|
|
}, "bar")
|
|
|
|
return err
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
|
|
|
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
|
|
|
mock.ExpectBegin()
|
|
|
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(2))
|
|
|
|
mock.ExpectCommit()
|
|
|
|
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
conn := CachedConn{
|
|
|
|
db: sqlx.NewSqlConnFromDB(db),
|
|
|
|
cache: cache.NewNode(r, syncx.NewSingleFlight(), stats, sql.ErrNoRows),
|
|
|
|
}
|
|
|
|
assert.NoError(t, conn.Transact(func(session sqlx.Session) error {
|
|
|
|
var val string
|
|
|
|
conn = conn.WithSession(session)
|
|
|
|
err := conn.QueryRow(&val, "foo", func(conn sqlx.SqlConn, v interface{}) error {
|
|
|
|
return conn.QueryRow(v, "any")
|
|
|
|
})
|
|
|
|
assert.Equal(t, "2", val)
|
|
|
|
return err
|
|
|
|
}))
|
|
|
|
val, err := r.Get("foo")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, `"2"`, val)
|
|
|
|
})
|
|
|
|
|
|
|
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
|
|
|
mock.ExpectBegin()
|
|
|
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(2))
|
|
|
|
mock.ExpectExec("any").WillReturnResult(sqlmock.NewResult(2, 3))
|
|
|
|
mock.ExpectCommit()
|
|
|
|
|
|
|
|
r := redistest.CreateRedis(t)
|
|
|
|
conn := CachedConn{
|
|
|
|
db: sqlx.NewSqlConnFromDB(db),
|
|
|
|
cache: cache.NewNode(r, syncx.NewSingleFlight(), stats, sql.ErrNoRows),
|
|
|
|
}
|
|
|
|
assert.NoError(t, conn.Transact(func(session sqlx.Session) error {
|
|
|
|
var val string
|
|
|
|
conn = conn.WithSession(session)
|
|
|
|
assert.NoError(t, conn.QueryRow(&val, "foo", func(conn sqlx.SqlConn, v interface{}) error {
|
|
|
|
return conn.QueryRow(v, "any")
|
|
|
|
}))
|
|
|
|
assert.Equal(t, "2", val)
|
|
|
|
_, err := conn.Exec(func(conn sqlx.SqlConn) (sql.Result, error) {
|
|
|
|
return conn.Exec("any")
|
|
|
|
}, "foo")
|
|
|
|
return err
|
|
|
|
}))
|
|
|
|
val, err := r.Get("foo")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Empty(t, val)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func resetStats() {
|
|
|
|
atomic.StoreUint64(&stats.Total, 0)
|
|
|
|
atomic.StoreUint64(&stats.Hit, 0)
|
|
|
|
atomic.StoreUint64(&stats.Miss, 0)
|
|
|
|
atomic.StoreUint64(&stats.DbFails, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
type dummySqlConn struct {
|
|
|
|
queryRow func(any, string, ...any) error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) ExecCtx(_ context.Context, _ string, _ ...any) (sql.Result, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) PrepareCtx(_ context.Context, _ string) (sqlx.StmtSession, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRowPartialCtx(_ context.Context, _ any, _ string, _ ...any) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRowsCtx(_ context.Context, _ any, _ string, _ ...any) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRowsPartialCtx(_ context.Context, _ any, _ string, _ ...any) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) TransactCtx(_ context.Context, _ func(context.Context, sqlx.Session) error) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) Exec(_ string, _ ...any) (sql.Result, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) Prepare(_ string) (sqlx.StmtSession, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRow(v any, query string, args ...any) error {
|
|
|
|
return d.QueryRowCtx(context.Background(), v, query, args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRowCtx(_ context.Context, v any, query string, args ...any) error {
|
|
|
|
if d.queryRow != nil {
|
|
|
|
return d.queryRow(v, query, args...)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRowPartial(_ any, _ string, _ ...any) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRows(_ any, _ string, _ ...any) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) QueryRowsPartial(_ any, _ string, _ ...any) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) RawDB() (*sql.DB, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d dummySqlConn) Transact(func(session sqlx.Session) error) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type trackedConn struct {
|
|
|
|
dummySqlConn
|
|
|
|
execValue bool
|
|
|
|
queryRowsValue bool
|
|
|
|
transactValue bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) Exec(query string, args ...any) (sql.Result, error) {
|
|
|
|
return c.ExecCtx(context.Background(), query, args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) ExecCtx(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
|
|
|
c.execValue = true
|
|
|
|
return c.dummySqlConn.ExecCtx(ctx, query, args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) QueryRows(v any, query string, args ...any) error {
|
|
|
|
return c.QueryRowsCtx(context.Background(), v, query, args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) QueryRowsCtx(ctx context.Context, v any, query string, args ...any) error {
|
|
|
|
c.queryRowsValue = true
|
|
|
|
return c.dummySqlConn.QueryRowsCtx(ctx, v, query, args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) RawDB() (*sql.DB, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) Transact(fn func(session sqlx.Session) error) error {
|
|
|
|
return c.TransactCtx(context.Background(), func(_ context.Context, session sqlx.Session) error {
|
|
|
|
return fn(session)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *trackedConn) TransactCtx(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
|
|
|
|
c.transactValue = true
|
|
|
|
return c.dummySqlConn.TransactCtx(ctx, fn)
|
|
|
|
}
|