Compare commits

...

87 Commits

Author SHA1 Message Date
kevin
81bf122fa4 update breaker doc 2020-10-17 22:58:30 +08:00
firefantasy
a14bd309a9 to correct breaker interface annotation (#136) 2020-10-17 22:55:36 +08:00
kevin
ea7e410145 update doc 2020-10-17 19:25:30 +08:00
kevin
e81358e7fa update doc 2020-10-17 19:20:01 +08:00
kevin
695ea69bfc add logx.Alert 2020-10-17 19:11:01 +08:00
kevin
d2ed14002c add fx.Split 2020-10-17 12:51:46 +08:00
kingxt
1d9c4a4c4b add anonymous annotation (#134)
* rebase upstream

* rebase

* trim no need line

* trim no need line

* trim no need line

* update doc

* remove update

* remove no need

* remove no need

* goctl add jwt support

* goctl add jwt support

* goctl add jwt support

* goctl support import

* goctl support import

* support return ()

* revert

* refactor and rename folder to group

* remove no need

* add anonymous annotation

* optimized

* rename

* rename

* update test

* optimized new command

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-10-16 19:35:18 +08:00
mywaystay
7e83895c6e update goctl rpc template log print url (#133) 2020-10-16 16:21:22 +08:00
kingxt
dc0534573c print more message when parse error (#131)
* rebase upstream

* rebase

* trim no need line

* trim no need line

* trim no need line

* update doc

* remove update

* remove no need

* remove no need

* goctl add jwt support

* goctl add jwt support

* goctl add jwt support

* goctl support import

* goctl support import

* support return ()

* revert

* refactor and rename folder to group

* print more error info when parse error

* remove no need

* refactor

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-10-16 15:56:29 +08:00
kevin
fe3739b7f3 fix golint issues 2020-10-16 11:13:55 +08:00
kevin
94645481b1 fix golint issues 2020-10-16 10:50:43 +08:00
sjatsh
338caf9927 delete goctl rpc main tpl no use import (#130) 2020-10-16 10:44:04 +08:00
kevin
9cc979960f update doc 2020-10-15 17:39:49 +08:00
kevin
f904710811 support api templates 2020-10-15 16:36:49 +08:00
kevin
8291eabc2c assert len > 0 2020-10-15 14:25:10 +08:00
codingfanlt
901fadb5d3 fix: fx/fn.Head func will forever block when n is less than 1 (#128)
* fix fx/Stream Head func will forever block when n is less than 1

* update test case

* update test case
2020-10-15 14:10:37 +08:00
kevin
c824e9e118 fail fast when rolling window size is zero 2020-10-15 11:40:31 +08:00
codingfanlt
6f49639f80 fix syncx/barrier test case (#123) 2020-10-13 19:29:20 +08:00
Keson
7d4a548d29 fix: template cache key (#121) 2020-10-12 14:34:11 +08:00
kevin
936dd67008 simplify code generation 2020-10-12 11:39:50 +08:00
super_mario
84cc41df42 stop rpc server when main function exit (#120)
add defer s.Stop() to mainTemplate, in order to stop rpc server when main function exit
2020-10-12 11:37:43 +08:00
kevin
da1a93e932 faster the tests 2020-10-11 22:07:50 +08:00
Keson
7e61555d42 Gozero sqlgen patch (#119)
* merge upstream

* optimize insert logic

* reactor functions
2020-10-11 21:55:44 +08:00
kevin
7a134ec64d update readme 2020-10-11 20:13:03 +08:00
kevin
d123b00e73 add qq qrcode 2020-10-11 20:02:06 +08:00
kevin
20d53add46 update readme 2020-10-11 19:42:40 +08:00
kevin
a1b141d31a make tests faster 2020-10-10 18:22:49 +08:00
Keson
0a9c427443 Goctl rpc patch (#117)
* remove mock generation

* add: proto project import

* update document

* remove mock generation

* add: proto project import

* update document

* remove NL

* update document

* optimize code

* add test

* add test
2020-10-10 16:19:46 +08:00
kevin
c32759d735 make tests race-free 2020-10-10 15:36:07 +08:00
kevin
fe855c52f1 avoid bigint converted into float64 when unmarshaling 2020-10-10 15:24:29 +08:00
kevin
3f8b080882 add more tests 2020-10-10 13:47:55 +08:00
kevin
adc275872d add more tests 2020-10-10 11:53:49 +08:00
kevin
be39133dba fix data race in tests 2020-10-09 19:13:10 +08:00
kingxt
15a9ab1d18 parser ad test (#116)
* rebase upstream

* rebase

* trim no need line

* trim no need line

* trim no need line

* update doc

* remove update

* remove no need

* remove no need

* goctl add jwt support

* goctl add jwt support

* goctl add jwt support

* goctl support import

* goctl support import

* support return ()

* revert

* refactor and rename folder to group

* parser add test

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-10-09 16:03:00 +08:00
kevin
7c354dcc38 add more tests 2020-10-09 14:53:13 +08:00
kevin
3733b06f1b fix data race in tests 2020-10-09 14:15:27 +08:00
kevin
8115a0932e add more tests 2020-10-09 13:59:38 +08:00
kevin
4df5eb760c add more tests 2020-10-08 22:39:07 +08:00
kevin
4a639b853c add more tests 2020-10-08 09:42:20 +08:00
kevin
1023425c1d add more tests 2020-10-07 23:15:34 +08:00
kevin
360fbfd0fa add more tests 2020-10-07 23:02:58 +08:00
kevin
09b7625f06 add more tests 2020-10-07 22:54:51 +08:00
kevin
6db294b5cc add more tests 2020-10-07 19:33:52 +08:00
kevin
305b6749fd add more tests 2020-10-07 19:13:19 +08:00
kevin
10b855713d add more tests 2020-10-07 19:00:15 +08:00
kevin
1cc0f071d9 add more tests 2020-10-07 18:07:54 +08:00
kevin
02ce8f82c8 add more tests 2020-10-07 11:43:02 +08:00
kevin
8a585afbf0 add more tests 2020-10-07 11:19:10 +08:00
kevin
e356025cef add more tests 2020-10-07 08:11:20 +08:00
kevin
14dee114dd add more tests 2020-10-06 10:12:35 +08:00
kevin
637a94a189 add fx.Count 2020-10-05 18:17:59 +08:00
kevin
173b347c90 add more tests 2020-10-05 12:19:54 +08:00
kevin
6749c5b94a add more tests 2020-10-04 17:52:54 +08:00
刘青
e66cca3710 breaker: remover useless code (#114) 2020-10-04 16:25:26 +08:00
kevin
f90c0aa98e update wechat qrcode 2020-10-04 10:14:08 +08:00
kevin
f00b5416a3 update codecov settings 2020-10-03 23:09:29 +08:00
kevin
f49694d6b6 fix data race 2020-10-02 22:41:25 +08:00
kevin
d809bf2dca add more tests 2020-10-02 22:37:15 +08:00
kevin
44ae5463bc add more tests 2020-10-02 09:00:25 +08:00
kevin
40dbd722d7 add more tests 2020-10-01 23:29:49 +08:00
kevin
709574133b add more tests 2020-10-01 23:22:53 +08:00
kevin
cb1c593108 remove markdown linter 2020-10-01 21:11:19 +08:00
kevin
6ecf575c00 add more tests 2020-10-01 20:58:12 +08:00
kevin
b8fcdd5460 add more tests 2020-10-01 17:50:53 +08:00
kevin
ce42281568 add more tests 2020-10-01 17:27:21 +08:00
kevin
40230d79e7 fix data race 2020-10-01 16:58:07 +08:00
kevin
ba7851795b add more tests 2020-10-01 16:49:39 +08:00
kevin
096fe3bc47 add more tests 2020-10-01 11:57:06 +08:00
kevin
e37858295a add more tests 2020-10-01 11:49:17 +08:00
kevin
5a4afb1518 add more tests 2020-10-01 10:29:03 +08:00
kevin
63f1f39c40 fix int64 primary key problem 2020-09-30 22:25:47 +08:00
kevin
481895d1e4 add more tests 2020-09-30 17:47:56 +08:00
shenbaise9527
9e9ce3bf48 GetBreaker need double-check (#112) 2020-09-30 16:50:02 +08:00
kevin
0ce654968d add more tests 2020-09-30 15:36:13 +08:00
Percy Gauguin
2703493541 update: fix wrong word (#110) 2020-09-30 15:08:47 +08:00
janetyu
d4240cd4b0 perfect the bookstore and shorturl doc (#109)
* perfect the bookstore and shorturl doc

* 避免歧义
2020-09-30 14:22:37 +08:00
kevin
a22bcc84a3 better lock practice in sharedcalls 2020-09-30 12:31:35 +08:00
kevin
93f430a449 update shorturl doc 2020-09-29 17:36:00 +08:00
kevin
d1b303fe7e export cache package, add client interceptor customization 2020-09-29 17:25:49 +08:00
kevin
dbca20e3df add zrpc client interceptor 2020-09-29 16:09:11 +08:00
boob
b3ead4d76c doc: update sharedcalls.md layout (#107) 2020-09-29 14:32:17 +08:00
kevin
33a9db85c8 add unit test, fix interceptor bug 2020-09-29 14:30:22 +08:00
kingxt
e7d46aa6e2 refactor and rename folder to group (#106)
* rebase upstream

* rebase

* trim no need line

* trim no need line

* trim no need line

* update doc

* remove update

* remove no need

* remove no need

* goctl add jwt support

* goctl add jwt support

* goctl add jwt support

* goctl support import

* goctl support import

* support return ()

* support return ()

* revert

* format api

* refactor and rename folder to group

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-09-29 11:14:52 +08:00
kevin
b282304054 add api doc link 2020-09-28 16:58:29 +08:00
bittoy
0a36031d48 use default mongo db (#103) 2020-09-28 16:35:07 +08:00
kevin
e5d7c3ab04 unmarshal should be struct 2020-09-28 15:19:30 +08:00
kevin
12c08bfd39 Revert "goreportcard not working, remove it temporarily"
This reverts commit 8f465fa439.
2020-09-28 11:41:23 +08:00
239 changed files with 5153 additions and 5241 deletions

View File

@@ -1,4 +1,4 @@
ignore:
- "doc"
- "example"
- "tools"
- "tools"

View File

@@ -1,6 +0,0 @@
{
"MD010": false,
"MD013": false,
"MD033": false,
"MD034": false
}

View File

@@ -37,7 +37,6 @@ type (
BloomFilter struct {
bits uint
maps uint
bitSet BitSetProvider
}
)

View File

@@ -13,11 +13,6 @@ import (
"github.com/tal-tech/go-zero/core/timex"
)
const (
StateClosed State = iota
StateOpen
)
const (
numHistoryReasons = 5
timeFormat = "15:04:05"
@@ -27,11 +22,10 @@ const (
var ErrServiceUnavailable = errors.New("circuit breaker is open")
type (
State = int32
Acceptable func(err error) bool
Breaker interface {
// Name returns the name of the netflixBreaker.
// Name returns the name of the Breaker.
Name() string
// Allow checks if the request is allowed.
@@ -40,34 +34,34 @@ type (
// If not allow, ErrServiceUnavailable will be returned.
Allow() (Promise, error)
// Do runs the given request if the netflixBreaker accepts it.
// Do returns an error instantly if the netflixBreaker rejects the request.
// If a panic occurs in the request, the netflixBreaker handles it as an error
// Do runs the given request if the Breaker accepts it.
// Do returns an error instantly if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again.
Do(req func() error) error
// DoWithAcceptable runs the given request if the netflixBreaker accepts it.
// Do returns an error instantly if the netflixBreaker rejects the request.
// If a panic occurs in the request, the netflixBreaker handles it as an error
// DoWithAcceptable runs the given request if the Breaker accepts it.
// DoWithAcceptable returns an error instantly if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again.
// acceptable checks if it's a successful call, even if the err is not nil.
DoWithAcceptable(req func() error, acceptable Acceptable) error
// DoWithFallback runs the given request if the netflixBreaker accepts it.
// DoWithFallback runs the fallback if the netflixBreaker rejects the request.
// If a panic occurs in the request, the netflixBreaker handles it as an error
// DoWithFallback runs the given request if the Breaker accepts it.
// DoWithFallback runs the fallback if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again.
DoWithFallback(req func() error, fallback func(err error) error) error
// DoWithFallbackAcceptable runs the given request if the netflixBreaker accepts it.
// DoWithFallback runs the fallback if the netflixBreaker rejects the request.
// If a panic occurs in the request, the netflixBreaker handles it as an error
// DoWithFallbackAcceptable runs the given request if the Breaker accepts it.
// DoWithFallbackAcceptable runs the fallback if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again.
// acceptable checks if it's a successful call, even if the err is not nil.
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error
}
BreakerOption func(breaker *circuitBreaker)
Option func(breaker *circuitBreaker)
Promise interface {
Accept()
@@ -95,7 +89,7 @@ type (
}
)
func NewBreaker(opts ...BreakerOption) Breaker {
func NewBreaker(opts ...Option) Breaker {
var b circuitBreaker
for _, opt := range opts {
opt(&b)
@@ -133,7 +127,7 @@ func (cb *circuitBreaker) Name() string {
return cb.name
}
func WithName(name string) BreakerOption {
func WithName(name string) Option {
return func(b *circuitBreaker) {
b.name = name
}

View File

@@ -41,10 +41,13 @@ func GetBreaker(name string) Breaker {
}
lock.Lock()
defer lock.Unlock()
b, ok = breakers[name]
if !ok {
b = NewBreaker(WithName(name))
breakers[name] = b
}
lock.Unlock()
b = NewBreaker()
breakers[name] = b
return b
}
@@ -55,20 +58,5 @@ func NoBreakFor(name string) {
}
func do(name string, execute func(b Breaker) error) error {
lock.RLock()
b, ok := breakers[name]
lock.RUnlock()
if ok {
return execute(b)
}
lock.Lock()
b, ok = breakers[name]
if !ok {
b = NewBreaker(WithName(name))
breakers[name] = b
}
lock.Unlock()
return execute(b)
return execute(GetBreaker(name))
}

View File

@@ -2,7 +2,6 @@ package breaker
import (
"math"
"sync/atomic"
"time"
"github.com/tal-tech/go-zero/core/collection"
@@ -21,7 +20,6 @@ const (
// see Client-Side Throttling section in https://landing.google.com/sre/sre-book/chapters/handling-overload/
type googleBreaker struct {
k float64
state int32
stat *collection.RollingWindow
proba *mathx.Proba
}
@@ -32,7 +30,6 @@ func newGoogleBreaker() *googleBreaker {
return &googleBreaker{
stat: st,
k: k,
state: StateClosed,
proba: mathx.NewProba(),
}
}
@@ -43,15 +40,9 @@ func (b *googleBreaker) accept() error {
// https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101
dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
if dropRatio <= 0 {
if atomic.LoadInt32(&b.state) == StateOpen {
atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed)
}
return nil
}
if atomic.LoadInt32(&b.state) == StateClosed {
atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen)
}
if b.proba.TrueOnProba(dropRatio) {
return ErrServiceUnavailable
}

View File

@@ -2,7 +2,6 @@ package breaker
import (
"errors"
"math"
"math/rand"
"testing"
"time"
@@ -27,7 +26,6 @@ func getGoogleBreaker() *googleBreaker {
return &googleBreaker{
stat: st,
k: 5,
state: StateClosed,
proba: mathx.NewProba(),
}
}
@@ -158,7 +156,7 @@ func TestGoogleBreakerSelfProtection(t *testing.T) {
t.Run("total request > 100, total < 2 * success", func(t *testing.T) {
b := getGoogleBreaker()
size := rand.Intn(10000)
accepts := int(math.Ceil(float64(size))) + 1
accepts := size + 1
markSuccess(b, accepts)
markFailed(b, size-accepts)
assert.Nil(t, b.accept())

View File

@@ -0,0 +1,80 @@
package cmdline
import (
"fmt"
"os"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/iox"
"github.com/tal-tech/go-zero/core/lang"
)
func TestEnterToContinue(t *testing.T) {
restore, err := iox.RedirectInOut()
assert.Nil(t, err)
defer restore()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println()
}()
go func() {
defer wg.Done()
EnterToContinue()
}()
wait := make(chan lang.PlaceholderType)
go func() {
wg.Wait()
close(wait)
}()
select {
case <-time.After(time.Second):
t.Error("timeout")
case <-wait:
}
}
func TestReadLine(t *testing.T) {
r, w, err := os.Pipe()
assert.Nil(t, err)
ow := os.Stdout
os.Stdout = w
or := os.Stdin
os.Stdin = r
defer func() {
os.Stdin = or
os.Stdout = ow
}()
const message = "hello"
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println(message)
}()
go func() {
defer wg.Done()
input := ReadLine("")
assert.Equal(t, message, input)
}()
wait := make(chan lang.PlaceholderType)
go func() {
wg.Wait()
close(wait)
}()
select {
case <-time.After(time.Second):
t.Error("timeout")
case <-wait:
}
}

View File

@@ -71,3 +71,12 @@ func TestDiffieHellmanMiddleManAttack(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, string(src), string(decryptedSrc))
}
func TestKeyBytes(t *testing.T) {
var empty DhKey
assert.Equal(t, 0, len(empty.Bytes()))
key, err := GenerateKey()
assert.Nil(t, err)
assert.True(t, len(key.Bytes()) > 0)
}

View File

@@ -6,6 +6,8 @@ import (
"io"
)
const unzipLimit = 100 * 1024 * 1024 // 100MB
func Gzip(bs []byte) []byte {
var b bytes.Buffer
@@ -24,8 +26,7 @@ func Gunzip(bs []byte) ([]byte, error) {
defer r.Close()
var c bytes.Buffer
_, err = io.Copy(&c, r)
if err != nil {
if _, err = io.Copy(&c, io.LimitReader(r, unzipLimit)); err != nil {
return nil, err
}

19
core/codec/hmac_test.go Normal file
View File

@@ -0,0 +1,19 @@
package codec
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHmac(t *testing.T) {
ret := Hmac([]byte("foo"), "bar")
assert.Equal(t, "f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
fmt.Sprintf("%x", ret))
}
func TestHmacBase64(t *testing.T) {
ret := HmacBase64([]byte("foo"), "bar")
assert.Equal(t, "+TILrwJJFp5zhQzWFW3tAQbiu2rYyrAbe7vr5tEGUxc=", ret)
}

58
core/codec/rsa_test.go Normal file
View File

@@ -0,0 +1,58 @@
package codec
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/fs"
)
const (
priKey = `-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC4TJk3onpqb2RYE3wwt23J9SHLFstHGSkUYFLe+nl1dEKHbD+/
Zt95L757J3xGTrwoTc7KCTxbrgn+stn0w52BNjj/kIE2ko4lbh/v8Fl14AyVR9ms
fKtKOnhe5FCT72mdtApr+qvzcC3q9hfXwkyQU32pv7q5UimZ205iKSBmgQIDAQAB
AoGAM5mWqGIAXj5z3MkP01/4CDxuyrrGDVD5FHBno3CDgyQa4Gmpa4B0/ywj671B
aTnwKmSmiiCN2qleuQYASixes2zY5fgTzt+7KNkl9JHsy7i606eH2eCKzsUa/s6u
WD8V3w/hGCQ9zYI18ihwyXlGHIgcRz/eeRh+nWcWVJzGOPUCQQD5nr6It/1yHb1p
C6l4fC4xXF19l4KxJjGu1xv/sOpSx0pOqBDEX3Mh//FU954392rUWDXV1/I65BPt
TLphdsu3AkEAvQJ2Qay/lffFj9FaUrvXuftJZ/Ypn0FpaSiUh3Ak3obBT6UvSZS0
bcYdCJCNHDtBOsWHnIN1x+BcWAPrdU7PhwJBAIQ0dUlH2S3VXnoCOTGc44I1Hzbj
Rc65IdsuBqA3fQN2lX5vOOIog3vgaFrOArg1jBkG1wx5IMvb/EnUN2pjVqUCQCza
KLXtCInOAlPemlCHwumfeAvznmzsWNdbieOZ+SXVVIpR6KbNYwOpv7oIk3Pfm9sW
hNffWlPUKhW42Gc+DIECQQDmk20YgBXwXWRM5DRPbhisIV088N5Z58K9DtFWkZsd
OBDT3dFcgZONtlmR1MqZO0pTh30lA4qovYj3Bx7A8i36
-----END RSA PRIVATE KEY-----`
pubKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4TJk3onpqb2RYE3wwt23J9SHL
FstHGSkUYFLe+nl1dEKHbD+/Zt95L757J3xGTrwoTc7KCTxbrgn+stn0w52BNjj/
kIE2ko4lbh/v8Fl14AyVR9msfKtKOnhe5FCT72mdtApr+qvzcC3q9hfXwkyQU32p
v7q5UimZ205iKSBmgQIDAQAB
-----END PUBLIC KEY-----`
testBody = `this is the content`
)
func TestCryption(t *testing.T) {
enc, err := NewRsaEncrypter([]byte(pubKey))
assert.Nil(t, err)
ret, err := enc.Encrypt([]byte(testBody))
assert.Nil(t, err)
file, err := fs.TempFilenameWithText(priKey)
assert.Nil(t, err)
dec, err := NewRsaDecrypter(file)
assert.Nil(t, err)
actual, err := dec.Decrypt(ret)
assert.Nil(t, err)
assert.Equal(t, testBody, string(actual))
actual, err = dec.DecryptBase64(base64.StdEncoding.EncodeToString(ret))
assert.Nil(t, err)
assert.Equal(t, testBody, string(actual))
}
func TestBadPubKey(t *testing.T) {
_, err := NewRsaEncrypter([]byte("foo"))
assert.Equal(t, ErrPublicKey, err)
}

View File

@@ -29,7 +29,6 @@ type (
name string
lock sync.Mutex
data map[string]interface{}
evicts *list.List
expire time.Duration
timingWheel *TimingWheel
lruCache lru
@@ -278,18 +277,15 @@ func (cs *cacheStat) statLoop() {
ticker := time.NewTicker(statInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
hit := atomic.SwapUint64(&cs.hit, 0)
miss := atomic.SwapUint64(&cs.miss, 0)
total := hit + miss
if total == 0 {
continue
}
percent := 100 * float32(hit) / float32(total)
logx.Statf("cache(%s) - qpm: %d, hit_ratio: %.1f%%, elements: %d, hit: %d, miss: %d",
cs.name, total, percent, cs.sizeCallback(), hit, miss)
for range ticker.C {
hit := atomic.SwapUint64(&cs.hit, 0)
miss := atomic.SwapUint64(&cs.miss, 0)
total := hit + miss
if total == 0 {
continue
}
percent := 100 * float32(hit) / float32(total)
logx.Statf("cache(%s) - qpm: %d, hit_ratio: %.1f%%, elements: %d, hit: %d, miss: %d",
cs.name, total, percent, cs.sizeCallback(), hit, miss)
}
}

View File

@@ -6,6 +6,10 @@ type Ring struct {
}
func NewRing(n int) *Ring {
if n < 1 {
panic("n should be greater than 0")
}
return &Ring{
elements: make([]interface{}, n),
}

View File

@@ -6,6 +6,12 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewRing(t *testing.T) {
assert.Panics(t, func() {
NewRing(0)
})
}
func TestRingLess(t *testing.T) {
ring := NewRing(5)
for i := 0; i < 3; i++ {

View File

@@ -22,6 +22,10 @@ type (
)
func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOption) *RollingWindow {
if size < 1 {
panic("size must be greater than 0")
}
w := &RollingWindow{
size: size,
win: newWindow(size),

View File

@@ -11,6 +11,13 @@ import (
const duration = time.Millisecond * 50
func TestNewRollingWindow(t *testing.T) {
assert.NotNil(t, NewRollingWindow(10, time.Second))
assert.Panics(t, func() {
NewRollingWindow(0, time.Second)
})
}
func TestRollingWindowAdd(t *testing.T) {
const size = 3
r := NewRollingWindow(size, duration)
@@ -81,7 +88,7 @@ func TestRollingWindowReduce(t *testing.T) {
for _, test := range tests {
t.Run(stringx.Rand(), func(t *testing.T) {
r := test.win
for x := 0; x < size; x = x + 1 {
for x := 0; x < size; x++ {
for i := 0; i <= x; i++ {
r.Add(float64(i))
}

View File

@@ -15,6 +15,7 @@ const (
stringType
)
// Set is not thread-safe, for concurrent use, make sure to use it with synchronization.
type Set struct {
data map[interface{}]lang.PlaceholderType
tp int
@@ -182,10 +183,7 @@ func (s *Set) add(i interface{}) {
}
func (s *Set) setType(i interface{}) {
if s.tp != untyped {
return
}
// s.tp can only be untyped here
switch i.(type) {
case int:
s.tp = intType

View File

@@ -5,8 +5,13 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
)
func init() {
logx.Disable()
}
func BenchmarkRawSet(b *testing.B) {
m := make(map[interface{}]struct{})
for i := 0; i < b.N; i++ {
@@ -147,3 +152,51 @@ func TestCount(t *testing.T) {
// then
assert.Equal(t, set.Count(), 3)
}
func TestKeysIntMismatch(t *testing.T) {
set := NewSet()
set.add(int64(1))
set.add(2)
vals := set.KeysInt()
assert.EqualValues(t, []int{2}, vals)
}
func TestKeysInt64Mismatch(t *testing.T) {
set := NewSet()
set.add(1)
set.add(int64(2))
vals := set.KeysInt64()
assert.EqualValues(t, []int64{2}, vals)
}
func TestKeysUintMismatch(t *testing.T) {
set := NewSet()
set.add(1)
set.add(uint(2))
vals := set.KeysUint()
assert.EqualValues(t, []uint{2}, vals)
}
func TestKeysUint64Mismatch(t *testing.T) {
set := NewSet()
set.add(1)
set.add(uint64(2))
vals := set.KeysUint64()
assert.EqualValues(t, []uint64{2}, vals)
}
func TestKeysStrMismatch(t *testing.T) {
set := NewSet()
set.add(1)
set.add("2")
vals := set.KeysStr()
assert.EqualValues(t, []string{"2"}, vals)
}
func TestSetType(t *testing.T) {
set := NewUnmanagedSet()
set.add(1)
set.add("2")
vals := set.Keys()
assert.ElementsMatch(t, []interface{}{1, "2"}, vals)
}

View File

@@ -32,8 +32,7 @@ func TestConfigJson(t *testing.T) {
A string `json:"a"`
B int `json:"b"`
}
err = LoadConfig(tmpfile, &val)
assert.Nil(t, err)
MustLoad(tmpfile, &val)
assert.Equal(t, "foo", val.A)
assert.Equal(t, 1, val.B)
})

View File

@@ -30,12 +30,12 @@ type mapBasedProperties struct {
lock sync.RWMutex
}
// Loads the properties into a properties configuration instance. May return the
// configuration itself along with an error that indicates if there was a problem loading the configuration.
// Loads the properties into a properties configuration instance.
// Returns an error that indicates if there was a problem loading the configuration.
func LoadProperties(filename string) (Properties, error) {
lines, err := iox.ReadTextLines(filename, iox.WithoutBlank(), iox.OmitWithPrefix("#"))
if err != nil {
return nil, nil
return nil, err
}
raw := make(map[string]string)

View File

@@ -41,3 +41,8 @@ func TestSetInt(t *testing.T) {
props.SetInt(key, value)
assert.Equal(t, value, props.GetInt(key))
}
func TestLoadBadFile(t *testing.T) {
_, err := LoadProperties("nosuchfile")
assert.NotNil(t, err)
}

View File

@@ -10,8 +10,10 @@ import (
func TestShrinkDeadlineLess(t *testing.T) {
deadline := time.Now().Add(time.Second)
ctx, _ := context.WithDeadline(context.Background(), deadline)
ctx, _ = ShrinkDeadline(ctx, time.Minute)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
ctx, cancel = ShrinkDeadline(ctx, time.Minute)
defer cancel()
dl, ok := ctx.Deadline()
assert.True(t, ok)
assert.Equal(t, deadline, dl)
@@ -19,8 +21,10 @@ func TestShrinkDeadlineLess(t *testing.T) {
func TestShrinkDeadlineMore(t *testing.T) {
deadline := time.Now().Add(time.Minute)
ctx, _ := context.WithDeadline(context.Background(), deadline)
ctx, _ = ShrinkDeadline(ctx, time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
ctx, cancel = ShrinkDeadline(ctx, time.Second)
defer cancel()
dl, ok := ctx.Deadline()
assert.True(t, ok)
assert.True(t, dl.Before(deadline))

View File

@@ -12,7 +12,8 @@ func TestContextCancel(t *testing.T) {
c := context.WithValue(context.Background(), "key", "value")
c1, cancel := context.WithCancel(c)
o := ValueOnlyFrom(c1)
c2, _ := context.WithCancel(o)
c2, cancel2 := context.WithCancel(o)
defer cancel2()
contexts := []context.Context{c1, c2}
for _, c := range contexts {
@@ -35,7 +36,8 @@ func TestContextCancel(t *testing.T) {
}
func TestContextDeadline(t *testing.T) {
c, _ := context.WithDeadline(context.Background(), time.Now().Add(10*time.Millisecond))
c, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Millisecond))
cancel()
o := ValueOnlyFrom(c)
select {
case <-time.After(100 * time.Millisecond):
@@ -43,9 +45,11 @@ func TestContextDeadline(t *testing.T) {
t.Fatal("ValueOnlyContext: context should not have timed out")
}
c, _ = context.WithDeadline(context.Background(), time.Now().Add(10*time.Millisecond))
c, cancel = context.WithDeadline(context.Background(), time.Now().Add(10*time.Millisecond))
cancel()
o = ValueOnlyFrom(c)
c, _ = context.WithDeadline(o, time.Now().Add(20*time.Millisecond))
c, cancel = context.WithDeadline(o, time.Now().Add(20*time.Millisecond))
defer cancel()
select {
case <-time.After(100 * time.Millisecond):
t.Fatal("ValueOnlyContext+Deadline: context should have timed out")

View File

@@ -8,7 +8,7 @@ import (
)
const (
indexOfKey = iota
_ = iota
indexOfId
)

View File

@@ -1,4 +1,3 @@
//go:generate mockgen -package internal -destination listener_mock.go -source listener.go Listener
package internal
type Listener interface {

View File

@@ -1,45 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: listener.go
// Package internal is a generated GoMock package.
package internal
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockListener is a mock of Listener interface
type MockListener struct {
ctrl *gomock.Controller
recorder *MockListenerMockRecorder
}
// MockListenerMockRecorder is the mock recorder for MockListener
type MockListenerMockRecorder struct {
mock *MockListener
}
// NewMockListener creates a new mock instance
func NewMockListener(ctrl *gomock.Controller) *MockListener {
mock := &MockListener{ctrl: ctrl}
mock.recorder = &MockListenerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockListener) EXPECT() *MockListenerMockRecorder {
return m.recorder
}
// OnUpdate mocks base method
func (m *MockListener) OnUpdate(keys, values []string, newKey string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "OnUpdate", keys, values, newKey)
}
// OnUpdate indicates an expected call of OnUpdate
func (mr *MockListenerMockRecorder) OnUpdate(keys, values, newKey interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnUpdate", reflect.TypeOf((*MockListener)(nil).OnUpdate), keys, values, newKey)
}

View File

@@ -111,6 +111,10 @@ func TestPublisher_keepAliveAsyncQuit(t *testing.T) {
defer ctrl.Finish()
const id clientv3.LeaseID = 1
cli := internal.NewMockEtcdClient(ctrl)
cli.EXPECT().ActiveConnection()
cli.EXPECT().Close()
defer cli.Close()
cli.ActiveConnection()
restore := setMockClient(cli)
defer restore()
cli.EXPECT().Ctx().AnyTimes()

View File

@@ -86,9 +86,7 @@ func TestBuldExecutorFlushSlowTasks(t *testing.T) {
time.Sleep(time.Millisecond * 100)
lock.Lock()
defer lock.Unlock()
for _, i := range tasks {
result = append(result, i)
}
result = append(result, tasks...)
}, WithBulkTasks(1000))
for i := 0; i < total; i++ {
assert.Nil(t, exec.Add(i))

View File

@@ -68,6 +68,7 @@ func Range(source <-chan interface{}) Stream {
}
// Buffer buffers the items into a queue with size n.
// It can balance the producer and the consumer if their processing throughput don't match.
func (p Stream) Buffer(n int) Stream {
if n < 0 {
n = 0
@@ -84,6 +85,14 @@ func (p Stream) Buffer(n int) Stream {
return Range(source)
}
// Count counts the number of elements in the result.
func (p Stream) Count() (count int) {
for range p.source {
count++
}
return
}
// Distinct removes the duplicated items base on the given KeyFunc.
func (p Stream) Distinct(fn KeyFunc) Stream {
source := make(chan interface{})
@@ -151,6 +160,10 @@ func (p Stream) Group(fn KeyFunc) Stream {
}
func (p Stream) Head(n int64) Stream {
if n < 1 {
panic("n must be greater than 0")
}
source := make(chan interface{})
go func() {
@@ -235,7 +248,37 @@ func (p Stream) Sort(less LessFunc) Stream {
return Just(items...)
}
// Split splits the elements into chunk with size up to n,
// might be less than n on tailing elements.
func (p Stream) Split(n int) Stream {
if n < 1 {
panic("n should be greater than 0")
}
source := make(chan interface{})
go func() {
var chunk []interface{}
for item := range p.source {
chunk = append(chunk, item)
if len(chunk) == n {
source <- chunk
chunk = nil
}
}
if chunk != nil {
source <- chunk
}
close(source)
}()
return Range(source)
}
func (p Stream) Tail(n int64) Stream {
if n < 1 {
panic("n should be greater than 0")
}
source := make(chan interface{})
go func() {

View File

@@ -49,6 +49,36 @@ func TestBufferNegative(t *testing.T) {
assert.Equal(t, 10, result)
}
func TestCount(t *testing.T) {
tests := []struct {
name string
elements []interface{}
}{
{
name: "no elements with nil",
},
{
name: "no elements",
elements: []interface{}{},
},
{
name: "1 element",
elements: []interface{}{1},
},
{
name: "multiple elements",
elements: []interface{}{1, 2, 3},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
val := Just(test.elements...).Count()
assert.Equal(t, len(test.elements), val)
})
}
}
func TestDone(t *testing.T) {
var count int32
Just(1, 2, 3).Walk(func(item interface{}, pipe chan<- interface{}) {
@@ -139,6 +169,14 @@ func TestHead(t *testing.T) {
assert.Equal(t, 3, result)
}
func TestHeadZero(t *testing.T) {
assert.Panics(t, func() {
Just(1, 2, 3, 4).Head(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
return nil, nil
})
})
}
func TestHeadMore(t *testing.T) {
var result int
Just(1, 2, 3, 4).Head(6).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
@@ -245,6 +283,22 @@ func TestSort(t *testing.T) {
})
}
func TestSplit(t *testing.T) {
assert.Panics(t, func() {
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(0).Done()
})
var chunks [][]interface{}
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(4).ForEach(func(item interface{}) {
chunk := item.([]interface{})
chunks = append(chunks, chunk)
})
assert.EqualValues(t, [][]interface{}{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10},
}, chunks)
}
func TestTail(t *testing.T) {
var result int
Just(1, 2, 3, 4).Tail(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
@@ -256,6 +310,14 @@ func TestTail(t *testing.T) {
assert.Equal(t, 7, result)
}
func TestTailZero(t *testing.T) {
assert.Panics(t, func() {
Just(1, 2, 3, 4).Tail(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
return nil, nil
})
})
}
func TestWalk(t *testing.T) {
var result int
Just(1, 2, 3, 4, 5).Walk(func(item interface{}, pipe chan<- interface{}) {

23
core/iox/pipe.go Normal file
View File

@@ -0,0 +1,23 @@
package iox
import "os"
// RedirectInOut redirects stdin to r, stdout to w, and callers need to call restore afterwards.
func RedirectInOut() (restore func(), err error) {
var r, w *os.File
r, w, err = os.Pipe()
if err != nil {
return
}
ow := os.Stdout
os.Stdout = w
or := os.Stdin
os.Stdin = r
restore = func() {
os.Stdin = or
os.Stdout = ow
}
return
}

13
core/iox/pipe_test.go Normal file
View File

@@ -0,0 +1,13 @@
package iox
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRedirectInOut(t *testing.T) {
restore, err := RedirectInOut()
assert.Nil(t, err)
defer restore()
}

View File

@@ -153,13 +153,10 @@ func (lim *TokenLimiter) waitForRedis() {
lim.rescueLock.Unlock()
}()
for {
select {
case <-ticker.C:
if lim.store.Ping() {
atomic.StoreUint32(&lim.redisAlive, 1)
return
}
for range ticker.C {
if lim.store.Ping() {
atomic.StoreUint32(&lim.redisAlive, 1)
return
}
}
}

View File

@@ -135,6 +135,7 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
passCounter: passCounter,
rtCounter: rtCounter,
windows: buckets,
dropTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.NewAtomicBool(),
}
// cpu >= 800, inflight < maxPass
@@ -160,6 +161,40 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
}
shedder.avgFlying = 80
assert.False(t, shedder.shouldDrop())
// cpu >= 800, inflight < maxPass
systemOverloadChecker = func(int64) bool {
return true
}
shedder.avgFlying = 80
shedder.flying = 80
_, err := shedder.Allow()
assert.NotNil(t, err)
}
func TestAdaptiveShedderStillHot(t *testing.T) {
logx.Disable()
passCounter := newRollingWindow()
rtCounter := newRollingWindow()
for i := 0; i < 10; i++ {
if i > 0 {
time.Sleep(bucketDuration)
}
passCounter.Add(float64((i + 1) * 100))
for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(float64(j))
}
}
shedder := &adaptiveShedder{
passCounter: passCounter,
rtCounter: rtCounter,
windows: buckets,
dropTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.ForAtomicBool(true),
}
assert.False(t, shedder.stillHot())
shedder.dropTime.Set(-coolOffDuration * 2)
assert.False(t, shedder.stillHot())
}
func BenchmarkAdaptiveShedder_Allow(b *testing.B) {

View File

@@ -13,3 +13,8 @@ func TestGroup(t *testing.T) {
assert.NotNil(t, limiter)
})
}
func TestShedderClose(t *testing.T) {
var nop nopCloser
assert.Nil(t, nop.Close())
}

View File

@@ -43,6 +43,7 @@ const (
consoleMode = "console"
volumeMode = "volume"
levelAlert = "alert"
levelInfo = "info"
levelError = "error"
levelSevere = "severe"
@@ -121,6 +122,10 @@ func SetUp(c LogConf) error {
}
}
func Alert(v string) {
output(errorLog, levelAlert, v)
}
func Close() error {
if writeConsole {
return nil

View File

@@ -6,8 +6,10 @@ import (
"io"
"io/ioutil"
"log"
"os"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -21,10 +23,13 @@ var (
)
type mockWriter struct {
lock sync.Mutex
builder strings.Builder
}
func (mw *mockWriter) Write(data []byte) (int, error) {
mw.lock.Lock()
defer mw.lock.Unlock()
return mw.builder.Write(data)
}
@@ -32,12 +37,22 @@ func (mw *mockWriter) Close() error {
return nil
}
func (mw *mockWriter) Contains(text string) bool {
mw.lock.Lock()
defer mw.lock.Unlock()
return strings.Contains(mw.builder.String(), text)
}
func (mw *mockWriter) Reset() {
mw.lock.Lock()
defer mw.lock.Unlock()
mw.builder.Reset()
}
func (mw *mockWriter) Contains(text string) bool {
return strings.Contains(mw.builder.String(), text)
func (mw *mockWriter) String() string {
mw.lock.Lock()
defer mw.lock.Unlock()
return mw.builder.String()
}
func TestFileLineFileMode(t *testing.T) {
@@ -69,6 +84,14 @@ func TestFileLineConsoleMode(t *testing.T) {
assert.True(t, writer.Contains(fmt.Sprintf("%s:%d", file, line+1)))
}
func TestStructedLogAlert(t *testing.T) {
doTestStructedLog(t, levelAlert, func(writer io.WriteCloser) {
errorLog = writer
}, func(v ...interface{}) {
Alert(fmt.Sprint(v...))
})
}
func TestStructedLogInfo(t *testing.T) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) {
infoLog = writer
@@ -85,6 +108,46 @@ func TestStructedLogSlow(t *testing.T) {
})
}
func TestStructedLogSlowf(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) {
slowLog = writer
}, func(v ...interface{}) {
Slowf(fmt.Sprint(v...))
})
}
func TestStructedLogStat(t *testing.T) {
doTestStructedLog(t, levelStat, func(writer io.WriteCloser) {
statLog = writer
}, func(v ...interface{}) {
Stat(v...)
})
}
func TestStructedLogStatf(t *testing.T) {
doTestStructedLog(t, levelStat, func(writer io.WriteCloser) {
statLog = writer
}, func(v ...interface{}) {
Statf(fmt.Sprint(v...))
})
}
func TestStructedLogSevere(t *testing.T) {
doTestStructedLog(t, levelSevere, func(writer io.WriteCloser) {
severeLog = writer
}, func(v ...interface{}) {
Severe(v...)
})
}
func TestStructedLogSeveref(t *testing.T) {
doTestStructedLog(t, levelSevere, func(writer io.WriteCloser) {
severeLog = writer
}, func(v ...interface{}) {
Severef(fmt.Sprint(v...))
})
}
func TestStructedLogWithDuration(t *testing.T) {
const message = "hello there"
writer := new(mockWriter)
@@ -135,6 +198,68 @@ func TestMustNil(t *testing.T) {
Must(nil)
}
func TestSetup(t *testing.T) {
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
})
MustSetup(LogConf{
ServiceName: "any",
Mode: "file",
Path: os.TempDir(),
})
MustSetup(LogConf{
ServiceName: "any",
Mode: "volume",
Path: os.TempDir(),
})
assert.NotNil(t, setupWithVolume(LogConf{}))
assert.NotNil(t, setupWithFiles(LogConf{}))
assert.Nil(t, setupWithFiles(LogConf{
ServiceName: "any",
Path: os.TempDir(),
Compress: true,
KeepDays: 1,
}))
setupLogLevel(LogConf{
Level: levelInfo,
})
setupLogLevel(LogConf{
Level: levelError,
})
setupLogLevel(LogConf{
Level: levelSevere,
})
_, err := createOutput("")
assert.NotNil(t, err)
Disable()
}
func TestDisable(t *testing.T) {
Disable()
var opt logOptions
WithKeepDays(1)(&opt)
WithGzip()(&opt)
assert.Nil(t, Close())
writeConsole = false
assert.Nil(t, Close())
}
func TestWithGzip(t *testing.T) {
fn := WithGzip()
var opt logOptions
fn(&opt)
assert.True(t, opt.gzipEnabled)
}
func TestWithKeepDays(t *testing.T) {
fn := WithKeepDays(1)
var opt logOptions
fn(&opt)
assert.Equal(t, 1, opt.keepDays)
}
func BenchmarkCopyByteSliceAppend(b *testing.B) {
for i := 0; i < b.N; i++ {
var buf []byte
@@ -232,7 +357,7 @@ func doTestStructedLog(t *testing.T, level string, setup func(writer io.WriteClo
t.Error(err)
}
assert.Equal(t, level, entry.Level)
assert.Equal(t, message, entry.Content)
assert.True(t, strings.Contains(entry.Content, message))
}
func testSetLevelTwiceWithMode(t *testing.T, mode string) {
@@ -252,4 +377,10 @@ func testSetLevelTwiceWithMode(t *testing.T, mode string) {
atomic.StoreUint32(&initialized, 1)
Info(message)
assert.Equal(t, 0, writer.builder.Len())
Infof(message)
assert.Equal(t, 0, writer.builder.Len())
ErrorStack(message)
assert.Equal(t, 0, writer.builder.Len())
ErrorStackf(message)
assert.Equal(t, 0, writer.builder.Len())
}

View File

@@ -192,14 +192,16 @@ func (l *RotateLogger) init() error {
}
func (l *RotateLogger) maybeCompressFile(file string) {
if l.compress {
defer func() {
if r := recover(); r != nil {
ErrorStack(r)
}
}()
compressLogFile(file)
if !l.compress {
return
}
defer func() {
if r := recover(); r != nil {
ErrorStack(r)
}
}()
compressLogFile(file)
}
func (l *RotateLogger) maybeDeleteOutdatedFiles() {

View File

@@ -0,0 +1,119 @@
package logx
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/fs"
)
func TestDailyRotateRuleMarkRotated(t *testing.T) {
var rule DailyRotateRule
rule.MarkRotated()
assert.Equal(t, getNowDate(), rule.rotatedTime)
}
func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
var rule DailyRotateRule
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
}
func TestDailyRotateRuleShallRotate(t *testing.T) {
var rule DailyRotateRule
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(dateFormat)
assert.True(t, rule.ShallRotate())
}
func TestRotateLoggerClose(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
assert.Nil(t, logger.Close())
}
func TestRotateLoggerGetBackupFilename(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
assert.True(t, len(logger.getBackupFilename()) > 0)
logger.backup = ""
assert.True(t, len(logger.getBackupFilename()) > 0)
}
func TestRotateLoggerMayCompressFile(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
logger.maybeCompressFile(filename)
_, err = os.Stat(filename)
assert.Nil(t, err)
}
func TestRotateLoggerMayCompressFileTrue(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
logger, err := NewLogger(filename, new(DailyRotateRule), true)
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(filename)
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
}
logger.maybeCompressFile(filename)
_, err = os.Stat(filename)
assert.NotNil(t, err)
}
func TestRotateLoggerRotate(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
logger, err := NewLogger(filename, new(DailyRotateRule), true)
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(filename)
os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
}
err = logger.rotate()
assert.Nil(t, err)
}
func TestRotateLoggerWrite(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
rule := new(DailyRotateRule)
logger, err := NewLogger(filename, rule, true)
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(filename)
os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
}
logger.write([]byte(`foo`))
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat)
logger.write([]byte(`bar`))
}

View File

@@ -15,7 +15,7 @@ const testlog = "Stay hungry, stay foolish."
func TestCollectSysLog(t *testing.T) {
CollectSysLog()
content := getContent(captureOutput(func() {
log.Printf(testlog)
log.Print(testlog)
}))
assert.True(t, strings.Contains(content, testlog))
}
@@ -33,10 +33,10 @@ func captureOutput(f func()) string {
writer := new(mockWriter)
infoLog = writer
prevLevel := logLevel
logLevel = InfoLevel
prevLevel := atomic.LoadUint32(&logLevel)
SetLevel(InfoLevel)
f()
logLevel = prevLevel
SetLevel(prevLevel)
return writer.builder.String()
}

View File

@@ -2,8 +2,11 @@ package logx
import (
"context"
"log"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/trace/tracespec"
@@ -17,13 +20,76 @@ const (
var mock tracespec.Trace = new(mockTrace)
func TestTraceLog(t *testing.T) {
var buf strings.Builder
var buf mockWriter
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
WithContext(ctx).(*traceLogger).write(&buf, levelInfo, testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
}
func TestTraceError(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
errorLog = newLogWriter(log.New(&buf, "", flags))
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Error(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
buf.Reset()
l.WithDuration(time.Second).Errorf(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
}
func TestTraceInfo(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
infoLog = newLogWriter(log.New(&buf, "", flags))
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
}
func TestTraceSlow(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
slowLog = newLogWriter(log.New(&buf, "", flags))
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Slow(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
buf.Reset()
l.WithDuration(time.Second).Slowf(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceId))
assert.True(t, strings.Contains(buf.String(), mockSpanId))
}
func TestTraceWithoutContext(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
infoLog = newLogWriter(log.New(&buf, "", flags))
l := WithContext(context.Background()).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.False(t, strings.Contains(buf.String(), mockTraceId))
assert.False(t, strings.Contains(buf.String(), mockSpanId))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.False(t, strings.Contains(buf.String(), mockTraceId))
assert.False(t, strings.Contains(buf.String(), mockSpanId))
}
type mockTrace struct{}
func (t mockTrace) TraceId() string {

View File

@@ -0,0 +1,31 @@
package mapping
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
type Bar struct {
Val string `json:"val"`
}
func TestFieldOptionOptionalDep(t *testing.T) {
var bar Bar
rt := reflect.TypeOf(bar)
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
val, opt, err := parseKeyAndOptions(jsonTagKey, field)
assert.Equal(t, "val", val)
assert.Nil(t, opt)
assert.Nil(t, err)
}
// check nil working
var o *fieldOptions
check := func(o *fieldOptions) {
assert.Equal(t, 0, len(o.optionalDep()))
}
check(o)
}

View File

@@ -23,6 +23,7 @@ const (
var (
errTypeMismatch = errors.New("type mismatch")
errValueNotSettable = errors.New("value is not settable")
errValueNotStruct = errors.New("value type is not struct")
keyUnmarshaler = NewUnmarshaler(defaultKeyName)
cacheKeys atomic.Value
cacheKeysLock sync.Mutex
@@ -80,6 +81,10 @@ func (u *Unmarshaler) unmarshalWithFullName(m Valuer, v interface{}, fullName st
}
rte := reflect.TypeOf(v).Elem()
if rte.Kind() != reflect.Struct {
return errValueNotStruct
}
rve := rv.Elem()
numFields := rte.NumField()
for i := 0; i < numFields; i++ {

View File

@@ -14,6 +14,13 @@ import (
// so we only can test to 62 bits.
const maxUintBitsToTest = 62
func TestUnmarshalWithFullNameNotStruct(t *testing.T) {
var s map[string]interface{}
content := []byte(`{"name":"xiaoming"}`)
err := UnmarshalJsonBytes(content, &s)
assert.Equal(t, errValueNotStruct, err)
}
func TestUnmarshalWithoutTagName(t *testing.T) {
type inner struct {
Optional bool `key:",optional"`
@@ -2380,6 +2387,13 @@ func TestUnmarshalNestedMapSimpleTypeMatch(t *testing.T) {
assert.Equal(t, "1", c.Anything["id"])
}
func TestUnmarshalValuer(t *testing.T) {
unmarshaler := NewUnmarshaler(jsonTagKey)
var foo string
err := unmarshaler.UnmarshalValuer(nil, foo)
assert.NotNil(t, err)
}
func BenchmarkUnmarshalString(b *testing.B) {
type inner struct {
Value string `key:"value"`

View File

@@ -31,6 +31,7 @@ func TestMaxInt(t *testing.T) {
}
for _, each := range cases {
each := each
t.Run(stringx.Rand(), func(t *testing.T) {
actual := MaxInt(each.a, each.b)
assert.Equal(t, each.expect, actual)

View File

@@ -0,0 +1,16 @@
package proc
import (
"log"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDumpGoroutines(t *testing.T) {
var buf strings.Builder
log.SetOutput(&buf)
dumpGoroutines()
assert.True(t, strings.Contains(buf.String(), ".dump"))
}

View File

@@ -26,10 +26,6 @@ var started uint32
// Profile represents an active profiling session.
type Profile struct {
// path holds the base path where various profiling files are written.
// If blank, the base path will be generated by ioutil.TempDir.
path string
// closers holds cleanup functions that run after each profile
closers []func()

21
core/proc/profile_test.go Normal file
View File

@@ -0,0 +1,21 @@
package proc
import (
"log"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProfile(t *testing.T) {
var buf strings.Builder
log.SetOutput(&buf)
profiler := StartProfile()
// start again should not work
assert.NotNil(t, StartProfile())
profiler.Stop()
// stop twice
profiler.Stop()
assert.True(t, strings.Contains(buf.String(), ".pprof"))
}

View File

@@ -27,7 +27,7 @@ func newMockedService(multiplier int) *mockedService {
func (s *mockedService) Start() {
mutex.Lock()
number = number * s.multiplier
number *= s.multiplier
mutex.Unlock()
done <- struct{}{}
<-s.quit

View File

@@ -11,6 +11,7 @@ import (
"time"
"github.com/tal-tech/go-zero/core/executors"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/proc"
"github.com/tal-tech/go-zero/core/sysx"
"github.com/tal-tech/go-zero/core/timex"
@@ -23,7 +24,7 @@ const (
)
var (
reporter func(string)
reporter = logx.Alert
lock sync.RWMutex
lessExecutor = executors.NewLessExecutor(time.Minute * 5)
dropped int32

22
core/stat/alert_test.go Normal file
View File

@@ -0,0 +1,22 @@
// +build linux
package stat
import (
"strconv"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
)
func TestReport(t *testing.T) {
var count int32
SetReporter(func(s string) {
atomic.AddInt32(&count, 1)
})
for i := 0; i < 10; i++ {
Report(strconv.Itoa(i))
}
assert.Equal(t, int32(1), count)
}

View File

@@ -1,6 +1,14 @@
package internal
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRefreshCpu(t *testing.T) {
assert.True(t, RefreshCpu() >= 0)
}
func BenchmarkRefreshCpu(b *testing.B) {
for i := 0; i < b.N; i++ {

37
core/stat/metrics_test.go Normal file
View File

@@ -0,0 +1,37 @@
package stat
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestMetrics(t *testing.T) {
counts := []int{1, 5, 10, 100, 1000, 1000}
for _, count := range counts {
m := NewMetrics("foo")
m.SetName("bar")
for i := 0; i < count; i++ {
m.Add(Task{
Duration: time.Millisecond * time.Duration(i),
Description: strconv.Itoa(i),
})
}
m.AddDrop()
var writer mockedWriter
SetReportWriter(&writer)
m.executor.Flush()
assert.Equal(t, "bar", writer.report.Name)
}
}
type mockedWriter struct {
report *StatReport
}
func (m *mockedWriter) Write(report *StatReport) error {
m.report = report
return nil
}

View File

@@ -0,0 +1,30 @@
package stat
import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/h2non/gock.v1"
)
func TestRemoteWriter(t *testing.T) {
defer gock.Off()
gock.New("http://foo.com").Reply(200).BodyString("foo")
writer := NewRemoteWriter("http://foo.com")
err := writer.Write(&StatReport{
Name: "bar",
})
assert.Nil(t, err)
}
func TestRemoteWriterFail(t *testing.T) {
defer gock.Off()
gock.New("http://foo.com").Reply(503).BodyString("foo")
writer := NewRemoteWriter("http://foo.com")
err := writer.Write(&StatReport{
Name: "bar",
})
assert.NotNil(t, err)
}

View File

@@ -1,4 +1,4 @@
package internal
package cache
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package internal
package cache
import (
"encoding/json"
@@ -8,7 +8,6 @@ import (
"testing"
"time"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/errorx"
"github.com/tal-tech/go-zero/core/hash"
@@ -76,12 +75,12 @@ func (mc *mockedNode) TakeWithExpire(v interface{}, key string, query func(v int
func TestCache_SetDel(t *testing.T) {
const total = 1000
r1 := miniredis.NewMiniRedis()
assert.Nil(t, r1.Start())
defer r1.Close()
r2 := miniredis.NewMiniRedis()
assert.Nil(t, r2.Start())
defer r2.Close()
r1, clean1, err := createMiniRedis()
assert.Nil(t, err)
defer clean1()
r2, clean2, err := createMiniRedis()
assert.Nil(t, err)
defer clean2()
conf := ClusterConf{
{
RedisConf: redis.RedisConf{
@@ -111,6 +110,45 @@ func TestCache_SetDel(t *testing.T) {
assert.Nil(t, c.GetCache(fmt.Sprintf("key/%d", i), &v))
assert.Equal(t, i, v)
}
assert.Nil(t, c.DelCache())
for i := 0; i < total; i++ {
assert.Nil(t, c.DelCache(fmt.Sprintf("key/%d", i)))
}
for i := 0; i < total; i++ {
var v int
assert.Equal(t, errPlaceholder, c.GetCache(fmt.Sprintf("key/%d", i), &v))
assert.Equal(t, 0, v)
}
}
func TestCache_OneNode(t *testing.T) {
const total = 1000
r, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
conf := ClusterConf{
{
RedisConf: redis.RedisConf{
Host: r.Addr(),
Type: redis.NodeType,
},
Weight: 100,
},
}
c := NewCache(conf, syncx.NewSharedCalls(), NewCacheStat("mock"), errPlaceholder)
for i := 0; i < total; i++ {
if i%2 == 0 {
assert.Nil(t, c.SetCache(fmt.Sprintf("key/%d", i), i))
} else {
assert.Nil(t, c.SetCacheWithExpire(fmt.Sprintf("key/%d", i), i, 0))
}
}
for i := 0; i < total; i++ {
var v int
assert.Nil(t, c.GetCache(fmt.Sprintf("key/%d", i), &v))
assert.Equal(t, i, v)
}
assert.Nil(t, c.DelCache())
for i := 0; i < total; i++ {
assert.Nil(t, c.DelCache(fmt.Sprintf("key/%d", i)))
}
@@ -188,6 +226,25 @@ func TestCache_Balance(t *testing.T) {
assert.Equal(t, total/10, count)
}
func TestCacheNoNode(t *testing.T) {
dispatcher := hash.NewConsistentHash()
c := cacheCluster{
dispatcher: dispatcher,
errNotFound: errPlaceholder,
}
assert.NotNil(t, c.DelCache("foo"))
assert.NotNil(t, c.DelCache("foo", "bar", "any"))
assert.NotNil(t, c.GetCache("foo", nil))
assert.NotNil(t, c.SetCache("foo", nil))
assert.NotNil(t, c.SetCacheWithExpire("foo", nil, time.Second))
assert.NotNil(t, c.Take(nil, "foo", func(v interface{}) error {
return nil
}))
assert.NotNil(t, c.TakeWithExpire(nil, "foo", func(v interface{}, duration time.Duration) error {
return nil
}))
}
func calcEntropy(m map[int]int, total int) float64 {
var entropy float64

View File

@@ -1,5 +1,3 @@
package cache
import "github.com/tal-tech/go-zero/core/stores/internal"
type CacheConf = internal.ClusterConf
type CacheConf = ClusterConf

View File

@@ -1,13 +1,13 @@
package internal
package cache
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"sync"
"time"
"github.com/tal-tech/go-zero/core/jsonx"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/mathx"
"github.com/tal-tech/go-zero/core/stat"
@@ -79,7 +79,7 @@ func (c cacheNode) SetCache(key string, v interface{}) error {
}
func (c cacheNode) SetCacheWithExpire(key string, v interface{}, expire time.Duration) error {
data, err := json.Marshal(v)
data, err := jsonx.Marshal(v)
if err != nil {
return err
}
@@ -168,7 +168,7 @@ func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) e
}
}
return json.Marshal(v)
return jsonx.Marshal(v)
})
if err != nil {
return err
@@ -181,11 +181,11 @@ func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) e
c.stat.IncrementHit()
}
return json.Unmarshal(val.([]byte), v)
return jsonx.Unmarshal(val.([]byte), v)
}
func (c cacheNode) processCache(key string, data string, v interface{}) error {
err := json.Unmarshal([]byte(data), v)
err := jsonx.Unmarshal([]byte(data), v)
if err == nil {
return nil
}

206
core/stores/cache/cachenode_test.go vendored Normal file
View File

@@ -0,0 +1,206 @@
package cache
import (
"errors"
"fmt"
"math/rand"
"strconv"
"sync"
"testing"
"time"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/mathx"
"github.com/tal-tech/go-zero/core/stat"
"github.com/tal-tech/go-zero/core/stores/redis"
"github.com/tal-tech/go-zero/core/syncx"
)
var errTestNotFound = errors.New("not found")
func init() {
logx.Disable()
stat.SetReporter(nil)
}
func TestCacheNode_DelCache(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errTestNotFound,
}
assert.Nil(t, cn.DelCache())
assert.Nil(t, cn.DelCache([]string{}...))
assert.Nil(t, cn.DelCache(make([]string, 0)...))
cn.SetCache("first", "one")
assert.Nil(t, cn.DelCache("first"))
cn.SetCache("first", "one")
cn.SetCache("second", "two")
assert.Nil(t, cn.DelCache("first", "second"))
}
func TestCacheNode_InvalidCache(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errTestNotFound,
}
s.Set("any", "value")
var str string
assert.NotNil(t, cn.GetCache("any", &str))
assert.Equal(t, "", str)
_, err = s.Get("any")
assert.Equal(t, miniredis.ErrKeyNotFound, err)
}
func TestCacheNode_Take(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
barrier: syncx.NewSharedCalls(),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errTestNotFound,
}
var str string
err = cn.Take(&str, "any", func(v interface{}) error {
*v.(*string) = "value"
return nil
})
assert.Nil(t, err)
assert.Equal(t, "value", str)
assert.Nil(t, cn.GetCache("any", &str))
val, err := s.Get("any")
assert.Nil(t, err)
assert.Equal(t, `"value"`, val)
}
func TestCacheNode_TakeNotFound(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
barrier: syncx.NewSharedCalls(),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errTestNotFound,
}
var str string
err = cn.Take(&str, "any", func(v interface{}) error {
return errTestNotFound
})
assert.Equal(t, errTestNotFound, err)
assert.Equal(t, errTestNotFound, cn.GetCache("any", &str))
val, err := s.Get("any")
assert.Nil(t, err)
assert.Equal(t, `*`, val)
s.Set("any", "*")
err = cn.Take(&str, "any", func(v interface{}) error {
return nil
})
assert.Equal(t, errTestNotFound, err)
assert.Equal(t, errTestNotFound, cn.GetCache("any", &str))
s.Del("any")
var errDummy = errors.New("dummy")
err = cn.Take(&str, "any", func(v interface{}) error {
return errDummy
})
assert.Equal(t, errDummy, err)
}
func TestCacheNode_TakeWithExpire(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
barrier: syncx.NewSharedCalls(),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errors.New("any"),
}
var str string
err = cn.TakeWithExpire(&str, "any", func(v interface{}, expire time.Duration) error {
*v.(*string) = "value"
return nil
})
assert.Nil(t, err)
assert.Equal(t, "value", str)
assert.Nil(t, cn.GetCache("any", &str))
val, err := s.Get("any")
assert.Nil(t, err)
assert.Equal(t, `"value"`, val)
}
func TestCacheNode_String(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
barrier: syncx.NewSharedCalls(),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errors.New("any"),
}
assert.Equal(t, s.Addr(), cn.String())
}
func TestCacheValueWithBigInt(t *testing.T) {
s, clean, err := createMiniRedis()
assert.Nil(t, err)
defer clean()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
barrier: syncx.NewSharedCalls(),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errors.New("any"),
}
const (
key = "key"
value int64 = 323427211229009810
)
assert.Nil(t, cn.SetCache(key, value))
var val interface{}
assert.Nil(t, cn.GetCache(key, &val))
assert.Equal(t, strconv.FormatInt(value, 10), fmt.Sprintf("%v", val))
}

View File

@@ -1,21 +1,45 @@
package cache
import (
"time"
import "time"
"github.com/tal-tech/go-zero/core/stores/internal"
const (
defaultExpiry = time.Hour * 24 * 7
defaultNotFoundExpiry = time.Minute
)
type Option = internal.Option
type (
Options struct {
Expiry time.Duration
NotFoundExpiry time.Duration
}
Option func(o *Options)
)
func newOptions(opts ...Option) Options {
var o Options
for _, opt := range opts {
opt(&o)
}
if o.Expiry <= 0 {
o.Expiry = defaultExpiry
}
if o.NotFoundExpiry <= 0 {
o.NotFoundExpiry = defaultNotFoundExpiry
}
return o
}
func WithExpiry(expiry time.Duration) Option {
return func(o *internal.Options) {
return func(o *Options) {
o.Expiry = expiry
}
}
func WithNotFoundExpiry(expiry time.Duration) Option {
return func(o *internal.Options) {
return func(o *Options) {
o.NotFoundExpiry = expiry
}
}

View File

@@ -1,4 +1,4 @@
package internal
package cache
import (
"sync/atomic"
@@ -48,20 +48,17 @@ func (cs *CacheStat) statLoop() {
ticker := time.NewTicker(statInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
total := atomic.SwapUint64(&cs.Total, 0)
if total == 0 {
continue
}
hit := atomic.SwapUint64(&cs.Hit, 0)
percent := 100 * float32(hit) / float32(total)
miss := atomic.SwapUint64(&cs.Miss, 0)
dbf := atomic.SwapUint64(&cs.DbFails, 0)
logx.Statf("dbcache(%s) - qpm: %d, hit_ratio: %.1f%%, hit: %d, miss: %d, db_fails: %d",
cs.name, total, percent, hit, miss, dbf)
for range ticker.C {
total := atomic.SwapUint64(&cs.Total, 0)
if total == 0 {
continue
}
hit := atomic.SwapUint64(&cs.Hit, 0)
percent := 100 * float32(hit) / float32(total)
miss := atomic.SwapUint64(&cs.Miss, 0)
dbf := atomic.SwapUint64(&cs.DbFails, 0)
logx.Statf("dbcache(%s) - qpm: %d, hit_ratio: %.1f%%, hit: %d, miss: %d, db_fails: %d",
cs.name, total, percent, hit, miss, dbf)
}
}

View File

@@ -1,4 +1,4 @@
package internal
package cache
import (
"fmt"

56
core/stores/cache/cleaner_test.go vendored Normal file
View File

@@ -0,0 +1,56 @@
package cache
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNextDelay(t *testing.T) {
tests := []struct {
name string
input time.Duration
output time.Duration
ok bool
}{
{
name: "second",
input: time.Second,
output: time.Second * 5,
ok: true,
},
{
name: "5 seconds",
input: time.Second * 5,
output: time.Minute,
ok: true,
},
{
name: "minute",
input: time.Minute,
output: time.Minute * 5,
ok: true,
},
{
name: "5 minutes",
input: time.Minute * 5,
output: time.Hour,
ok: true,
},
{
name: "hour",
input: time.Hour,
output: 0,
ok: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
next, ok := nextDelay(test.input)
assert.Equal(t, test.ok, ok)
assert.Equal(t, test.output, next)
})
}
}

View File

@@ -1,4 +1,4 @@
package internal
package cache
import "github.com/tal-tech/go-zero/core/stores/redis"

View File

@@ -1,4 +1,4 @@
package internal
package cache
import "strings"

48
core/stores/cache/util_test.go vendored Normal file
View File

@@ -0,0 +1,48 @@
package cache
import (
"testing"
"time"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/lang"
)
func TestFormatKeys(t *testing.T) {
assert.Equal(t, "a,b", formatKeys([]string{"a", "b"}))
}
func TestTotalWeights(t *testing.T) {
val := TotalWeights([]NodeConf{
{
Weight: -1,
},
{
Weight: 0,
},
{
Weight: 1,
},
})
assert.Equal(t, 1, val)
}
func createMiniRedis() (r *miniredis.Miniredis, clean func(), err error) {
r, err = miniredis.Run()
if err != nil {
return nil, nil, err
}
return r, func() {
ch := make(chan lang.PlaceholderType)
go func() {
r.Close()
close(ch)
}()
select {
case <-ch:
case <-time.After(time.Second):
}
}, nil
}

View File

@@ -1,65 +0,0 @@
package internal
import (
"errors"
"math/rand"
"sync"
"testing"
"time"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/mathx"
"github.com/tal-tech/go-zero/core/stat"
"github.com/tal-tech/go-zero/core/stores/redis"
)
func init() {
logx.Disable()
stat.SetReporter(nil)
}
func TestCacheNode_DelCache(t *testing.T) {
s, err := miniredis.Run()
assert.Nil(t, err)
defer s.Close()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errors.New("any"),
}
assert.Nil(t, cn.DelCache())
assert.Nil(t, cn.DelCache([]string{}...))
assert.Nil(t, cn.DelCache(make([]string, 0)...))
cn.SetCache("first", "one")
assert.Nil(t, cn.DelCache("first"))
cn.SetCache("first", "one")
cn.SetCache("second", "two")
assert.Nil(t, cn.DelCache("first", "second"))
}
func TestCacheNode_InvalidCache(t *testing.T) {
s, err := miniredis.Run()
assert.Nil(t, err)
defer s.Close()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewCacheStat("any"),
errNotFound: errors.New("any"),
}
s.Set("any", "value")
var str string
assert.NotNil(t, cn.GetCache("any", &str))
assert.Equal(t, "", str)
_, err = s.Get("any")
assert.Equal(t, miniredis.ErrKeyNotFound, err)
}

View File

@@ -1,33 +0,0 @@
package internal
import "time"
const (
defaultExpiry = time.Hour * 24 * 7
defaultNotFoundExpiry = time.Minute
)
type (
Options struct {
Expiry time.Duration
NotFoundExpiry time.Duration
}
Option func(o *Options)
)
func newOptions(opts ...Option) Options {
var o Options
for _, opt := range opts {
opt(&o)
}
if o.Expiry <= 0 {
o.Expiry = defaultExpiry
}
if o.NotFoundExpiry <= 0 {
o.NotFoundExpiry = defaultNotFoundExpiry
}
return o
}

View File

@@ -1,5 +1,7 @@
package kv
import "github.com/tal-tech/go-zero/core/stores/internal"
import (
"github.com/tal-tech/go-zero/core/stores/cache"
)
type KvConf = internal.ClusterConf
type KvConf = cache.ClusterConf

View File

@@ -6,7 +6,7 @@ import (
"github.com/tal-tech/go-zero/core/errorx"
"github.com/tal-tech/go-zero/core/hash"
"github.com/tal-tech/go-zero/core/stores/internal"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/redis"
)
@@ -81,7 +81,7 @@ type (
)
func NewStore(c KvConf) Store {
if len(c) == 0 || internal.TotalWeights(c) <= 0 {
if len(c) == 0 || cache.TotalWeights(c) <= 0 {
log.Fatal("no cache nodes")
}

View File

@@ -6,7 +6,8 @@ import (
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stores/internal"
"github.com/tal-tech/go-zero/core/hash"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/redis"
"github.com/tal-tech/go-zero/core/stringx"
)
@@ -15,6 +16,10 @@ var s1, _ = miniredis.Run()
var s2, _ = miniredis.Run()
func TestRedis_Exists(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Exists("foo")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
ok, err := client.Exists("a")
assert.Nil(t, err)
@@ -27,6 +32,10 @@ func TestRedis_Exists(t *testing.T) {
}
func TestRedis_Eval(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Eval(`redis.call("EXISTS", KEYS[1])`, "key1")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
_, err := client.Eval(`redis.call("EXISTS", KEYS[1])`, "notexist")
assert.Equal(t, redis.Nil, err)
@@ -41,6 +50,12 @@ func TestRedis_Eval(t *testing.T) {
}
func TestRedis_Hgetall(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
err := store.Hset("a", "aa", "aaa")
assert.NotNil(t, err)
_, err = store.Hgetall("a")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
@@ -54,6 +69,10 @@ func TestRedis_Hgetall(t *testing.T) {
}
func TestRedis_Hvals(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Hvals("a")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
@@ -64,6 +83,10 @@ func TestRedis_Hvals(t *testing.T) {
}
func TestRedis_Hsetnx(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Hsetnx("a", "dd", "ddd")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
@@ -80,6 +103,12 @@ func TestRedis_Hsetnx(t *testing.T) {
}
func TestRedis_HdelHlen(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Hdel("a", "aa")
assert.NotNil(t, err)
_, err = store.Hlen("a")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
@@ -96,6 +125,10 @@ func TestRedis_HdelHlen(t *testing.T) {
}
func TestRedis_HIncrBy(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Hincrby("key", "field", 3)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Hincrby("key", "field", 2)
assert.Nil(t, err)
@@ -107,6 +140,10 @@ func TestRedis_HIncrBy(t *testing.T) {
}
func TestRedis_Hkeys(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Hkeys("a")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
@@ -117,6 +154,10 @@ func TestRedis_Hkeys(t *testing.T) {
}
func TestRedis_Hmget(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Hmget("a", "aa", "bb")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
@@ -130,6 +171,12 @@ func TestRedis_Hmget(t *testing.T) {
}
func TestRedis_Hmset(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
err := store.Hmset("a", map[string]string{
"aa": "aaa",
})
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
assert.Nil(t, client.Hmset("a", map[string]string{
"aa": "aaa",
@@ -142,6 +189,10 @@ func TestRedis_Hmset(t *testing.T) {
}
func TestRedis_Incr(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Incr("a")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Incr("a")
assert.Nil(t, err)
@@ -153,6 +204,10 @@ func TestRedis_Incr(t *testing.T) {
}
func TestRedis_IncrBy(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Incrby("a", 2)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Incrby("a", 2)
assert.Nil(t, err)
@@ -164,6 +219,20 @@ func TestRedis_IncrBy(t *testing.T) {
}
func TestRedis_List(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Lpush("key", "value1", "value2")
assert.NotNil(t, err)
_, err = store.Rpush("key", "value3", "value4")
assert.NotNil(t, err)
_, err = store.Llen("key")
assert.NotNil(t, err)
_, err = store.Lrange("key", 0, 10)
assert.NotNil(t, err)
_, err = store.Lpop("key")
assert.NotNil(t, err)
_, err = store.Lrem("key", 0, "val")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Lpush("key", "value1", "value2")
assert.Nil(t, err)
@@ -202,6 +271,14 @@ func TestRedis_List(t *testing.T) {
}
func TestRedis_Persist(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Persist("key")
assert.NotNil(t, err)
err = store.Expire("key", 5)
assert.NotNil(t, err)
err = store.Expireat("key", time.Now().Unix()+5)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
ok, err := client.Persist("key")
assert.Nil(t, err)
@@ -225,8 +302,16 @@ func TestRedis_Persist(t *testing.T) {
}
func TestRedis_Sscan(t *testing.T) {
key := "list"
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Sadd(key, nil)
assert.NotNil(t, err)
_, _, err = store.Sscan(key, 0, "", 100)
assert.NotNil(t, err)
_, err = store.Del(key)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
key := "list"
var list []string
for i := 0; i < 1550; i++ {
list = append(list, stringx.Randn(i))
@@ -254,6 +339,20 @@ func TestRedis_Sscan(t *testing.T) {
}
func TestRedis_Set(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Scard("key")
assert.NotNil(t, err)
_, err = store.Sismember("key", 2)
assert.NotNil(t, err)
_, err = store.Srem("key", 3, 4)
assert.NotNil(t, err)
_, err = store.Smembers("key")
assert.NotNil(t, err)
_, err = store.Srandmember("key", 1)
assert.NotNil(t, err)
_, err = store.Spop("key")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
num, err := client.Sadd("key", 1, 2, 3, 4)
assert.Nil(t, err)
@@ -290,6 +389,14 @@ func TestRedis_Set(t *testing.T) {
}
func TestRedis_SetGetDel(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
err := store.Set("hello", "world")
assert.NotNil(t, err)
_, err = store.Get("hello")
assert.NotNil(t, err)
_, err = store.Del("hello")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
err := client.Set("hello", "world")
assert.Nil(t, err)
@@ -303,6 +410,16 @@ func TestRedis_SetGetDel(t *testing.T) {
}
func TestRedis_SetExNx(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
err := store.Setex("hello", "world", 5)
assert.NotNil(t, err)
_, err = store.Setnx("newhello", "newworld")
assert.NotNil(t, err)
_, err = store.Ttl("hello")
assert.NotNil(t, err)
_, err = store.SetnxEx("newhello", "newworld", 5)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
err := client.Setex("hello", "world", 5)
assert.Nil(t, err)
@@ -337,6 +454,16 @@ func TestRedis_SetExNx(t *testing.T) {
}
func TestRedis_SetGetDelHashField(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
err := store.Hset("key", "field", "value")
assert.NotNil(t, err)
_, err = store.Hget("key", "field")
assert.NotNil(t, err)
_, err = store.Hexists("key", "field")
assert.NotNil(t, err)
_, err = store.Hdel("key", "field")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
err := client.Hset("key", "field", "value")
assert.Nil(t, err)
@@ -356,6 +483,48 @@ func TestRedis_SetGetDelHashField(t *testing.T) {
}
func TestRedis_SortedSet(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Zadd("key", 1, "value1")
assert.NotNil(t, err)
_, err = store.Zscore("key", "value1")
assert.NotNil(t, err)
_, err = store.Zcount("key", 6, 7)
assert.NotNil(t, err)
_, err = store.Zincrby("key", 3, "value1")
assert.NotNil(t, err)
_, err = store.Zrank("key", "value2")
assert.NotNil(t, err)
_, err = store.Zrem("key", "value2", "value3")
assert.NotNil(t, err)
_, err = store.Zremrangebyscore("key", 6, 7)
assert.NotNil(t, err)
_, err = store.Zremrangebyrank("key", 1, 2)
assert.NotNil(t, err)
_, err = store.Zcard("key")
assert.NotNil(t, err)
_, err = store.Zrange("key", 0, -1)
assert.NotNil(t, err)
_, err = store.Zrevrange("key", 0, -1)
assert.NotNil(t, err)
_, err = store.ZrangeWithScores("key", 0, -1)
assert.NotNil(t, err)
_, err = store.ZrangebyscoreWithScores("key", 5, 8)
assert.NotNil(t, err)
_, err = store.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
assert.NotNil(t, err)
_, err = store.ZrevrangebyscoreWithScores("key", 5, 8)
assert.NotNil(t, err)
_, err = store.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
assert.NotNil(t, err)
_, err = store.Zadds("key", redis.Pair{
Key: "value2",
Score: 6,
}, redis.Pair{
Key: "value3",
Score: 7,
})
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
ok, err := client.Zadd("key", 1, "value1")
assert.Nil(t, err)
@@ -471,6 +640,30 @@ func TestRedis_SortedSet(t *testing.T) {
Score: 5,
},
}, pairs)
val, err = client.Zadds("key", redis.Pair{
Key: "value2",
Score: 6,
}, redis.Pair{
Key: "value3",
Score: 7,
})
assert.Nil(t, err)
assert.Equal(t, int64(2), val)
})
}
func TestRedis_HyperLogLog(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Pfadd("key")
assert.NotNil(t, err)
_, err = store.Pfcount("key")
assert.NotNil(t, err)
runOnCluster(t, func(cluster Store) {
_, err := cluster.Pfadd("key")
assert.NotNil(t, err)
_, err = cluster.Pfcount("key")
assert.NotNil(t, err)
})
}
@@ -478,7 +671,7 @@ func runOnCluster(t *testing.T, fn func(cluster Store)) {
s1.FlushAll()
s2.FlushAll()
store := NewStore([]internal.NodeConf{
store := NewStore([]cache.NodeConf{
{
RedisConf: redis.RedisConf{
Host: s1.Addr(),

View File

@@ -7,6 +7,7 @@ import (
"github.com/globalsign/mgo"
"github.com/tal-tech/go-zero/core/breaker"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/mongo/internal"
"github.com/tal-tech/go-zero/core/timex"
)
@@ -29,8 +30,9 @@ type (
}
decoratedCollection struct {
*mgo.Collection
brk breaker.Breaker
name string
collection internal.MgoCollection
brk breaker.Breaker
}
keepablePromise struct {
@@ -41,7 +43,8 @@ type (
func newCollection(collection *mgo.Collection) Collection {
return &decoratedCollection{
Collection: collection,
name: collection.FullName,
collection: collection,
brk: breaker.NewBreaker(),
}
}
@@ -54,7 +57,7 @@ func (c *decoratedCollection) Find(query interface{}) Query {
startTime := timex.Now()
return promisedQuery{
Query: c.Collection.Find(query),
Query: c.collection.Find(query),
promise: keepablePromise{
promise: promise,
log: func(err error) {
@@ -73,7 +76,7 @@ func (c *decoratedCollection) FindId(id interface{}) Query {
startTime := timex.Now()
return promisedQuery{
Query: c.Collection.FindId(id),
Query: c.collection.FindId(id),
promise: keepablePromise{
promise: promise,
log: func(err error) {
@@ -92,7 +95,7 @@ func (c *decoratedCollection) Insert(docs ...interface{}) (err error) {
c.logDuration("insert", duration, err, docs...)
}()
return c.Collection.Insert(docs...)
return c.collection.Insert(docs...)
}, acceptable)
}
@@ -104,7 +107,7 @@ func (c *decoratedCollection) Pipe(pipeline interface{}) Pipe {
startTime := timex.Now()
return promisedPipe{
Pipe: c.Collection.Pipe(pipeline),
Pipe: c.collection.Pipe(pipeline),
promise: keepablePromise{
promise: promise,
log: func(err error) {
@@ -123,7 +126,7 @@ func (c *decoratedCollection) Remove(selector interface{}) (err error) {
c.logDuration("remove", duration, err, selector)
}()
return c.Collection.Remove(selector)
return c.collection.Remove(selector)
}, acceptable)
}
@@ -135,7 +138,7 @@ func (c *decoratedCollection) RemoveAll(selector interface{}) (info *mgo.ChangeI
c.logDuration("removeAll", duration, err, selector)
}()
info, err = c.Collection.RemoveAll(selector)
info, err = c.collection.RemoveAll(selector)
return err
}, acceptable)
@@ -150,7 +153,7 @@ func (c *decoratedCollection) RemoveId(id interface{}) (err error) {
c.logDuration("removeId", duration, err, id)
}()
return c.Collection.RemoveId(id)
return c.collection.RemoveId(id)
}, acceptable)
}
@@ -162,7 +165,7 @@ func (c *decoratedCollection) Update(selector, update interface{}) (err error) {
c.logDuration("update", duration, err, selector, update)
}()
return c.Collection.Update(selector, update)
return c.collection.Update(selector, update)
}, acceptable)
}
@@ -174,7 +177,7 @@ func (c *decoratedCollection) UpdateId(id, update interface{}) (err error) {
c.logDuration("updateId", duration, err, id, update)
}()
return c.Collection.UpdateId(id, update)
return c.collection.UpdateId(id, update)
}, acceptable)
}
@@ -186,7 +189,7 @@ func (c *decoratedCollection) Upsert(selector, update interface{}) (info *mgo.Ch
c.logDuration("upsert", duration, err, selector, update)
}()
info, err = c.Collection.Upsert(selector, update)
info, err = c.collection.Upsert(selector, update)
return err
}, acceptable)
@@ -200,17 +203,17 @@ func (c *decoratedCollection) logDuration(method string, duration time.Duration,
} else if err != nil {
if duration > slowThreshold {
logx.WithDuration(duration).Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s) - %s",
c.FullName, method, err.Error(), string(content))
c.name, method, err.Error(), string(content))
} else {
logx.WithDuration(duration).Infof("mongo(%s) - %s - fail(%s) - %s",
c.FullName, method, err.Error(), string(content))
c.name, method, err.Error(), string(content))
}
} else {
if duration > slowThreshold {
logx.WithDuration(duration).Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s",
c.FullName, method, string(content))
c.name, method, string(content))
} else {
logx.WithDuration(duration).Infof("mongo(%s) - %s - ok - %s", c.FullName, method, string(content))
logx.WithDuration(duration).Infof("mongo(%s) - %s - ok - %s", c.name, method, string(content))
}
}
}

View File

@@ -5,10 +5,20 @@ import (
"testing"
"github.com/globalsign/mgo"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/breaker"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/mongo/internal"
"github.com/tal-tech/go-zero/core/stringx"
)
var errDummy = errors.New("dummy")
func init() {
logx.Disable()
}
func TestKeepPromise_accept(t *testing.T) {
p := new(mockPromise)
kp := keepablePromise{
@@ -56,6 +66,206 @@ func TestKeepPromise_keep(t *testing.T) {
}
}
func TestNewCollection(t *testing.T) {
col := newCollection(&mgo.Collection{
Database: nil,
Name: "foo",
FullName: "bar",
})
assert.Equal(t, "bar", col.(*decoratedCollection).name)
}
func TestCollectionFind(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var query mgo.Query
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().Find(gomock.Any()).Return(&query)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
actual := c.Find(nil)
switch v := actual.(type) {
case promisedQuery:
assert.Equal(t, &query, v.Query)
assert.Equal(t, errDummy, v.promise.keep(errDummy))
default:
t.Fail()
}
c.brk = new(dropBreaker)
actual = c.Find(nil)
assert.Equal(t, rejectedQuery{}, actual)
}
func TestCollectionFindId(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var query mgo.Query
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().FindId(gomock.Any()).Return(&query)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
actual := c.FindId(nil)
switch v := actual.(type) {
case promisedQuery:
assert.Equal(t, &query, v.Query)
assert.Equal(t, errDummy, v.promise.keep(errDummy))
default:
t.Fail()
}
c.brk = new(dropBreaker)
actual = c.FindId(nil)
assert.Equal(t, rejectedQuery{}, actual)
}
func TestCollectionInsert(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().Insert(nil, nil).Return(errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
err := c.Insert(nil, nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
err = c.Insert(nil, nil)
assert.Equal(t, errDummy, err)
}
func TestCollectionPipe(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var pipe mgo.Pipe
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().Pipe(gomock.Any()).Return(&pipe)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
actual := c.Pipe(nil)
switch v := actual.(type) {
case promisedPipe:
assert.Equal(t, &pipe, v.Pipe)
assert.Equal(t, errDummy, v.promise.keep(errDummy))
default:
t.Fail()
}
c.brk = new(dropBreaker)
actual = c.Pipe(nil)
assert.Equal(t, rejectedPipe{}, actual)
}
func TestCollectionRemove(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().Remove(gomock.Any()).Return(errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
err := c.Remove(nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
err = c.Remove(nil)
assert.Equal(t, errDummy, err)
}
func TestCollectionRemoveAll(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().RemoveAll(gomock.Any()).Return(nil, errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
_, err := c.RemoveAll(nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
_, err = c.RemoveAll(nil)
assert.Equal(t, errDummy, err)
}
func TestCollectionRemoveId(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().RemoveId(gomock.Any()).Return(errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
err := c.RemoveId(nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
err = c.RemoveId(nil)
assert.Equal(t, errDummy, err)
}
func TestCollectionUpdate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
err := c.Update(nil, nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
err = c.Update(nil, nil)
assert.Equal(t, errDummy, err)
}
func TestCollectionUpdateId(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().UpdateId(gomock.Any(), gomock.Any()).Return(errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
err := c.UpdateId(nil, nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
err = c.UpdateId(nil, nil)
assert.Equal(t, errDummy, err)
}
func TestCollectionUpsert(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
col := internal.NewMockMgoCollection(ctrl)
col.EXPECT().Upsert(gomock.Any(), gomock.Any()).Return(nil, errDummy)
c := decoratedCollection{
collection: col,
brk: breaker.NewBreaker(),
}
_, err := c.Upsert(nil, nil)
assert.Equal(t, errDummy, err)
c.brk = new(dropBreaker)
_, err = c.Upsert(nil, nil)
assert.Equal(t, errDummy, err)
}
type mockPromise struct {
accepted bool
reason string
@@ -68,3 +278,31 @@ func (p *mockPromise) Accept() {
func (p *mockPromise) Reject(reason string) {
p.reason = reason
}
type dropBreaker struct {
}
func (d *dropBreaker) Name() string {
return "dummy"
}
func (d *dropBreaker) Allow() (breaker.Promise, error) {
return nil, errDummy
}
func (d *dropBreaker) Do(req func() error) error {
return nil
}
func (d *dropBreaker) DoWithAcceptable(req func() error, acceptable breaker.Acceptable) error {
return errDummy
}
func (d *dropBreaker) DoWithFallback(req func() error, fallback func(err error) error) error {
return nil
}
func (d *dropBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error,
acceptable breaker.Acceptable) error {
return nil
}

View File

@@ -0,0 +1,17 @@
//go:generate mockgen -package internal -destination collection_mock.go -source collection.go
package internal
import "github.com/globalsign/mgo"
type MgoCollection interface {
Find(query interface{}) *mgo.Query
FindId(id interface{}) *mgo.Query
Insert(docs ...interface{}) error
Pipe(pipeline interface{}) *mgo.Pipe
Remove(selector interface{}) error
RemoveAll(selector interface{}) (*mgo.ChangeInfo, error)
RemoveId(id interface{}) error
Update(selector, update interface{}) error
UpdateId(id, update interface{}) error
Upsert(selector, update interface{}) (*mgo.ChangeInfo, error)
}

View File

@@ -0,0 +1,180 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: collection.go
// Package internal is a generated GoMock package.
package internal
import (
mgo "github.com/globalsign/mgo"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockMgoCollection is a mock of MgoCollection interface
type MockMgoCollection struct {
ctrl *gomock.Controller
recorder *MockMgoCollectionMockRecorder
}
// MockMgoCollectionMockRecorder is the mock recorder for MockMgoCollection
type MockMgoCollectionMockRecorder struct {
mock *MockMgoCollection
}
// NewMockMgoCollection creates a new mock instance
func NewMockMgoCollection(ctrl *gomock.Controller) *MockMgoCollection {
mock := &MockMgoCollection{ctrl: ctrl}
mock.recorder = &MockMgoCollectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockMgoCollection) EXPECT() *MockMgoCollectionMockRecorder {
return m.recorder
}
// Find mocks base method
func (m *MockMgoCollection) Find(query interface{}) *mgo.Query {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", query)
ret0, _ := ret[0].(*mgo.Query)
return ret0
}
// Find indicates an expected call of Find
func (mr *MockMgoCollectionMockRecorder) Find(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockMgoCollection)(nil).Find), query)
}
// FindId mocks base method
func (m *MockMgoCollection) FindId(id interface{}) *mgo.Query {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindId", id)
ret0, _ := ret[0].(*mgo.Query)
return ret0
}
// FindId indicates an expected call of FindId
func (mr *MockMgoCollectionMockRecorder) FindId(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindId", reflect.TypeOf((*MockMgoCollection)(nil).FindId), id)
}
// Insert mocks base method
func (m *MockMgoCollection) Insert(docs ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range docs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Insert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert
func (mr *MockMgoCollectionMockRecorder) Insert(docs ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockMgoCollection)(nil).Insert), docs...)
}
// Pipe mocks base method
func (m *MockMgoCollection) Pipe(pipeline interface{}) *mgo.Pipe {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Pipe", pipeline)
ret0, _ := ret[0].(*mgo.Pipe)
return ret0
}
// Pipe indicates an expected call of Pipe
func (mr *MockMgoCollectionMockRecorder) Pipe(pipeline interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pipe", reflect.TypeOf((*MockMgoCollection)(nil).Pipe), pipeline)
}
// Remove mocks base method
func (m *MockMgoCollection) Remove(selector interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", selector)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove
func (mr *MockMgoCollectionMockRecorder) Remove(selector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockMgoCollection)(nil).Remove), selector)
}
// RemoveAll mocks base method
func (m *MockMgoCollection) RemoveAll(selector interface{}) (*mgo.ChangeInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveAll", selector)
ret0, _ := ret[0].(*mgo.ChangeInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RemoveAll indicates an expected call of RemoveAll
func (mr *MockMgoCollectionMockRecorder) RemoveAll(selector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MockMgoCollection)(nil).RemoveAll), selector)
}
// RemoveId mocks base method
func (m *MockMgoCollection) RemoveId(id interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveId", id)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveId indicates an expected call of RemoveId
func (mr *MockMgoCollectionMockRecorder) RemoveId(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveId", reflect.TypeOf((*MockMgoCollection)(nil).RemoveId), id)
}
// Update mocks base method
func (m *MockMgoCollection) Update(selector, update interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", selector, update)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockMgoCollectionMockRecorder) Update(selector, update interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockMgoCollection)(nil).Update), selector, update)
}
// UpdateId mocks base method
func (m *MockMgoCollection) UpdateId(id, update interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateId", id, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateId indicates an expected call of UpdateId
func (mr *MockMgoCollectionMockRecorder) UpdateId(id, update interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateId", reflect.TypeOf((*MockMgoCollection)(nil).UpdateId), id, update)
}
// Upsert mocks base method
func (m *MockMgoCollection) Upsert(selector, update interface{}) (*mgo.ChangeInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upsert", selector, update)
ret0, _ := ret[0].(*mgo.ChangeInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Upsert indicates an expected call of Upsert
func (mr *MockMgoCollectionMockRecorder) Upsert(selector, update interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockMgoCollection)(nil).Upsert), selector, update)
}

View File

@@ -22,8 +22,8 @@ type (
}
)
func MustNewModel(url, database, collection string, opts ...Option) *Model {
model, err := NewModel(url, database, collection, opts...)
func MustNewModel(url, collection string, opts ...Option) *Model {
model, err := NewModel(url, collection, opts...)
if err != nil {
log.Fatal(err)
}
@@ -31,15 +31,16 @@ func MustNewModel(url, database, collection string, opts ...Option) *Model {
return model
}
func NewModel(url, database, collection string, opts ...Option) (*Model, error) {
func NewModel(url, collection string, opts ...Option) (*Model, error) {
session, err := getConcurrentSession(url)
if err != nil {
return nil, err
}
return &Model{
session: session,
db: session.DB(database),
session: session,
// If name is empty, the database name provided in the dialed URL is used instead
db: session.DB(""),
collection: collection,
opts: opts,
}, nil

View File

@@ -2,7 +2,7 @@ package mongoc
import (
"github.com/globalsign/mgo"
"github.com/tal-tech/go-zero/core/stores/internal"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/mongo"
"github.com/tal-tech/go-zero/core/syncx"
)
@@ -12,7 +12,7 @@ var (
// can't use one SharedCalls per conn, because multiple conns may share the same cache key.
sharedCalls = syncx.NewSharedCalls()
stats = internal.NewCacheStat("mongoc")
stats = cache.NewCacheStat("mongoc")
)
type (
@@ -20,11 +20,11 @@ type (
cachedCollection struct {
collection mongo.Collection
cache internal.Cache
cache cache.Cache
}
)
func newCollection(collection mongo.Collection, c internal.Cache) *cachedCollection {
func newCollection(collection mongo.Collection, c cache.Cache) *cachedCollection {
return &cachedCollection{
collection: collection,
cache: c,

View File

@@ -16,7 +16,7 @@ import (
"github.com/globalsign/mgo/bson"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stat"
"github.com/tal-tech/go-zero/core/stores/internal"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/mongo"
"github.com/tal-tech/go-zero/core/stores/redis"
)
@@ -33,7 +33,7 @@ func TestStat(t *testing.T) {
}
r := redis.NewRedis(s.Addr(), redis.NodeType)
cach := internal.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
c := newCollection(dummyConn{}, cach)
for i := 0; i < 10; i++ {
@@ -56,7 +56,7 @@ func TestStatCacheFails(t *testing.T) {
defer log.SetOutput(os.Stdout)
r := redis.NewRedis("localhost:59999", redis.NodeType)
cach := internal.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
c := newCollection(dummyConn{}, cach)
for i := 0; i < 20; i++ {
@@ -79,7 +79,7 @@ func TestStatDbFails(t *testing.T) {
}
r := redis.NewRedis(s.Addr(), redis.NodeType)
cach := internal.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
c := newCollection(dummyConn{}, cach)
for i := 0; i < 20; i++ {
@@ -103,7 +103,7 @@ func TestStatFromMemory(t *testing.T) {
}
r := redis.NewRedis(s.Addr(), redis.NodeType)
cach := internal.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
c := newCollection(dummyConn{}, cach)
var all sync.WaitGroup

View File

@@ -5,19 +5,18 @@ import (
"github.com/globalsign/mgo"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/internal"
"github.com/tal-tech/go-zero/core/stores/mongo"
"github.com/tal-tech/go-zero/core/stores/redis"
)
type Model struct {
*mongo.Model
cache internal.Cache
cache cache.Cache
generateCollection func(*mgo.Session) *cachedCollection
}
func MustNewNodeModel(url, database, collection string, rds *redis.Redis, opts ...cache.Option) *Model {
model, err := NewNodeModel(url, database, collection, rds, opts...)
func MustNewNodeModel(url, collection string, rds *redis.Redis, opts ...cache.Option) *Model {
model, err := NewNodeModel(url, collection, rds, opts...)
if err != nil {
log.Fatal(err)
}
@@ -25,8 +24,8 @@ func MustNewNodeModel(url, database, collection string, rds *redis.Redis, opts .
return model
}
func MustNewModel(url, database, collection string, c cache.CacheConf, opts ...cache.Option) *Model {
model, err := NewModel(url, database, collection, c, opts...)
func MustNewModel(url, collection string, c cache.CacheConf, opts ...cache.Option) *Model {
model, err := NewModel(url, collection, c, opts...)
if err != nil {
log.Fatal(err)
}
@@ -34,16 +33,16 @@ func MustNewModel(url, database, collection string, c cache.CacheConf, opts ...c
return model
}
func NewNodeModel(url, database, collection string, rds *redis.Redis, opts ...cache.Option) (*Model, error) {
c := internal.NewCacheNode(rds, sharedCalls, stats, mgo.ErrNotFound, opts...)
return createModel(url, database, collection, c, func(collection mongo.Collection) *cachedCollection {
func NewNodeModel(url, collection string, rds *redis.Redis, opts ...cache.Option) (*Model, error) {
c := cache.NewCacheNode(rds, sharedCalls, stats, mgo.ErrNotFound, opts...)
return createModel(url, collection, c, func(collection mongo.Collection) *cachedCollection {
return newCollection(collection, c)
})
}
func NewModel(url, database, collection string, conf cache.CacheConf, opts ...cache.Option) (*Model, error) {
c := internal.NewCache(conf, sharedCalls, stats, mgo.ErrNotFound, opts...)
return createModel(url, database, collection, c, func(collection mongo.Collection) *cachedCollection {
func NewModel(url, collection string, conf cache.CacheConf, opts ...cache.Option) (*Model, error) {
c := cache.NewCache(conf, sharedCalls, stats, mgo.ErrNotFound, opts...)
return createModel(url, collection, c, func(collection mongo.Collection) *cachedCollection {
return newCollection(collection, c)
})
}
@@ -224,9 +223,9 @@ func (mm *Model) pipe(fn func(c *cachedCollection) mongo.Pipe) (mongo.Pipe, erro
return fn(mm.GetCollection(session)), nil
}
func createModel(url, database, collection string, c internal.Cache,
func createModel(url, collection string, c cache.Cache,
create func(mongo.Collection) *cachedCollection) (*Model, error) {
model, err := mongo.NewModel(url, database, collection)
model, err := mongo.NewModel(url, collection)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,110 @@
package redis
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stringx"
)
func TestRedisConf(t *testing.T) {
tests := []struct {
name string
RedisConf
ok bool
}{
{
name: "missing host",
RedisConf: RedisConf{
Host: "",
Type: NodeType,
Pass: "",
},
ok: false,
},
{
name: "missing type",
RedisConf: RedisConf{
Host: "localhost:6379",
Type: "",
Pass: "",
},
ok: false,
},
{
name: "ok",
RedisConf: RedisConf{
Host: "localhost:6379",
Type: NodeType,
Pass: "",
},
ok: true,
},
}
for _, test := range tests {
t.Run(stringx.RandId(), func(t *testing.T) {
if test.ok {
assert.Nil(t, test.RedisConf.Validate())
assert.NotNil(t, test.RedisConf.NewRedis())
} else {
assert.NotNil(t, test.RedisConf.Validate())
}
})
}
}
func TestRedisKeyConf(t *testing.T) {
tests := []struct {
name string
RedisKeyConf
ok bool
}{
{
name: "missing host",
RedisKeyConf: RedisKeyConf{
RedisConf: RedisConf{
Host: "",
Type: NodeType,
Pass: "",
},
Key: "foo",
},
ok: false,
},
{
name: "missing key",
RedisKeyConf: RedisKeyConf{
RedisConf: RedisConf{
Host: "localhost:6379",
Type: NodeType,
Pass: "",
},
Key: "",
},
ok: false,
},
{
name: "ok",
RedisKeyConf: RedisKeyConf{
RedisConf: RedisConf{
Host: "localhost:6379",
Type: NodeType,
Pass: "",
},
Key: "foo",
},
ok: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.ok {
assert.Nil(t, test.RedisKeyConf.Validate())
} else {
assert.NotNil(t, test.RedisKeyConf.Validate())
}
})
}
}

View File

@@ -7,11 +7,14 @@ import (
"time"
"github.com/alicebob/miniredis"
red "github.com/go-redis/redis"
"github.com/stretchr/testify/assert"
)
func TestRedis_Exists(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Exists("a")
assert.NotNil(t, err)
ok, err := client.Exists("a")
assert.Nil(t, err)
assert.False(t, ok)
@@ -24,7 +27,9 @@ func TestRedis_Exists(t *testing.T) {
func TestRedis_Eval(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := client.Eval(`redis.call("EXISTS", KEYS[1])`, []string{"notexist"})
_, err := NewRedis(client.Addr, "").Eval(`redis.call("EXISTS", KEYS[1])`, []string{"notexist"})
assert.NotNil(t, err)
_, err = client.Eval(`redis.call("EXISTS", KEYS[1])`, []string{"notexist"})
assert.Equal(t, Nil, err)
err = client.Set("key1", "value1")
assert.Nil(t, err)
@@ -40,6 +45,8 @@ func TestRedis_Hgetall(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
_, err := NewRedis(client.Addr, "").Hgetall("a")
assert.NotNil(t, err)
vals, err := client.Hgetall("a")
assert.Nil(t, err)
assert.EqualValues(t, map[string]string{
@@ -51,8 +58,11 @@ func TestRedis_Hgetall(t *testing.T) {
func TestRedis_Hvals(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.NotNil(t, NewRedis(client.Addr, "").Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
_, err := NewRedis(client.Addr, "").Hvals("a")
assert.NotNil(t, err)
vals, err := client.Hvals("a")
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"aaa", "bbb"}, vals)
@@ -63,6 +73,8 @@ func TestRedis_Hsetnx(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
_, err := NewRedis(client.Addr, "").Hsetnx("a", "bb", "ccc")
assert.NotNil(t, err)
ok, err := client.Hsetnx("a", "bb", "ccc")
assert.Nil(t, err)
assert.False(t, ok)
@@ -79,6 +91,8 @@ func TestRedis_HdelHlen(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
_, err := NewRedis(client.Addr, "").Hlen("a")
assert.NotNil(t, err)
num, err := client.Hlen("a")
assert.Nil(t, err)
assert.Equal(t, 2, num)
@@ -93,6 +107,8 @@ func TestRedis_HdelHlen(t *testing.T) {
func TestRedis_HIncrBy(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Hincrby("key", "field", 2)
assert.NotNil(t, err)
val, err := client.Hincrby("key", "field", 2)
assert.Nil(t, err)
assert.Equal(t, 2, val)
@@ -106,6 +122,8 @@ func TestRedis_Hkeys(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
_, err := NewRedis(client.Addr, "").Hkeys("a")
assert.NotNil(t, err)
vals, err := client.Hkeys("a")
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"aa", "bb"}, vals)
@@ -116,6 +134,8 @@ func TestRedis_Hmget(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb"))
_, err := NewRedis(client.Addr, "").Hmget("a", "aa", "bb")
assert.NotNil(t, err)
vals, err := client.Hmget("a", "aa", "bb")
assert.Nil(t, err)
assert.EqualValues(t, []string{"aaa", "bbb"}, vals)
@@ -127,6 +147,7 @@ func TestRedis_Hmget(t *testing.T) {
func TestRedis_Hmset(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.NotNil(t, NewRedis(client.Addr, "").Hmset("a", nil))
assert.Nil(t, client.Hmset("a", map[string]string{
"aa": "aaa",
"bb": "bbb",
@@ -139,6 +160,8 @@ func TestRedis_Hmset(t *testing.T) {
func TestRedis_Incr(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Incr("a")
assert.NotNil(t, err)
val, err := client.Incr("a")
assert.Nil(t, err)
assert.Equal(t, int64(1), val)
@@ -150,6 +173,8 @@ func TestRedis_Incr(t *testing.T) {
func TestRedis_IncrBy(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Incrby("a", 2)
assert.NotNil(t, err)
val, err := client.Incrby("a", 2)
assert.Nil(t, err)
assert.Equal(t, int64(2), val)
@@ -165,26 +190,49 @@ func TestRedis_Keys(t *testing.T) {
assert.Nil(t, err)
err = client.Set("key2", "value2")
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").Keys("*")
assert.NotNil(t, err)
keys, err := client.Keys("*")
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"key1", "key2"}, keys)
})
}
func TestRedis_HyperLogLog(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
r := NewRedis(client.Addr, "")
_, err := r.Pfadd("key1")
assert.NotNil(t, err)
_, err = r.Pfcount("*")
assert.NotNil(t, err)
err = r.Pfmerge("*")
assert.NotNil(t, err)
})
}
func TestRedis_List(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Lpush("key", "value1", "value2")
assert.NotNil(t, err)
val, err := client.Lpush("key", "value1", "value2")
assert.Nil(t, err)
assert.Equal(t, 2, val)
_, err = NewRedis(client.Addr, "").Rpush("key", "value3", "value4")
assert.NotNil(t, err)
val, err = client.Rpush("key", "value3", "value4")
assert.Nil(t, err)
assert.Equal(t, 4, val)
_, err = NewRedis(client.Addr, "").Llen("key")
assert.NotNil(t, err)
val, err = client.Llen("key")
assert.Nil(t, err)
assert.Equal(t, 4, val)
vals, err := client.Lrange("key", 0, 10)
assert.Nil(t, err)
assert.EqualValues(t, []string{"value2", "value1", "value3", "value4"}, vals)
_, err = NewRedis(client.Addr, "").Lpop("key")
assert.NotNil(t, err)
v, err := client.Lpop("key")
assert.Nil(t, err)
assert.Equal(t, "value2", v)
@@ -194,9 +242,13 @@ func TestRedis_List(t *testing.T) {
val, err = client.Rpush("key", "value3", "value3")
assert.Nil(t, err)
assert.Equal(t, 7, val)
_, err = NewRedis(client.Addr, "").Lrem("key", 2, "value1")
assert.NotNil(t, err)
n, err := client.Lrem("key", 2, "value1")
assert.Nil(t, err)
assert.Equal(t, 2, n)
_, err = NewRedis(client.Addr, "").Lrange("key", 0, 10)
assert.NotNil(t, err)
vals, err = client.Lrange("key", 0, 10)
assert.Nil(t, err)
assert.EqualValues(t, []string{"value2", "value3", "value4", "value3", "value3"}, vals)
@@ -215,6 +267,8 @@ func TestRedis_Mget(t *testing.T) {
assert.Nil(t, err)
err = client.Set("key2", "value2")
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").Mget("key1", "key0", "key2", "key3")
assert.NotNil(t, err)
vals, err := client.Mget("key1", "key0", "key2", "key3")
assert.Nil(t, err)
assert.EqualValues(t, []string{"value1", "", "value2", ""}, vals)
@@ -223,7 +277,9 @@ func TestRedis_Mget(t *testing.T) {
func TestRedis_SetBit(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := client.SetBit("key", 1, 1)
err := NewRedis(client.Addr, "").SetBit("key", 1, 1)
assert.NotNil(t, err)
err = client.SetBit("key", 1, 1)
assert.Nil(t, err)
})
}
@@ -232,6 +288,8 @@ func TestRedis_GetBit(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := client.SetBit("key", 2, 1)
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").GetBit("key", 2)
assert.NotNil(t, err)
val, err := client.GetBit("key", 2)
assert.Nil(t, err)
assert.Equal(t, 1, val)
@@ -240,6 +298,8 @@ func TestRedis_GetBit(t *testing.T) {
func TestRedis_Persist(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Persist("key")
assert.NotNil(t, err)
ok, err := client.Persist("key")
assert.Nil(t, err)
assert.False(t, ok)
@@ -248,11 +308,15 @@ func TestRedis_Persist(t *testing.T) {
ok, err = client.Persist("key")
assert.Nil(t, err)
assert.False(t, ok)
err = NewRedis(client.Addr, "").Expire("key", 5)
assert.NotNil(t, err)
err = client.Expire("key", 5)
assert.Nil(t, err)
ok, err = client.Persist("key")
assert.Nil(t, err)
assert.True(t, ok)
err = NewRedis(client.Addr, "").Expireat("key", time.Now().Unix()+5)
assert.NotNil(t, err)
err = client.Expireat("key", time.Now().Unix()+5)
assert.Nil(t, err)
ok, err = client.Persist("key")
@@ -274,6 +338,8 @@ func TestRedis_Scan(t *testing.T) {
assert.Nil(t, err)
err = client.Set("key2", "value2")
assert.Nil(t, err)
_, _, err = NewRedis(client.Addr, "").Scan(0, "*", 100)
assert.NotNil(t, err)
keys, _, err := client.Scan(0, "*", 100)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"key1", "key2"}, keys)
@@ -294,6 +360,8 @@ func TestRedis_Sscan(t *testing.T) {
var cursor uint64 = 0
sum := 0
for {
_, _, err := NewRedis(client.Addr, "").Sscan(key, cursor, "", 100)
assert.NotNil(t, err)
keys, next, err := client.Sscan(key, cursor, "", 100)
assert.Nil(t, err)
sum += len(keys)
@@ -304,6 +372,8 @@ func TestRedis_Sscan(t *testing.T) {
}
assert.Equal(t, sum, 1550)
_, err = NewRedis(client.Addr, "").Del(key)
assert.NotNil(t, err)
_, err = client.Del(key)
assert.Nil(t, err)
})
@@ -311,46 +381,72 @@ func TestRedis_Sscan(t *testing.T) {
func TestRedis_Set(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := NewRedis(client.Addr, "").Sadd("key", 1, 2, 3, 4)
assert.NotNil(t, err)
num, err := client.Sadd("key", 1, 2, 3, 4)
assert.Nil(t, err)
assert.Equal(t, 4, num)
_, err = NewRedis(client.Addr, "").Scard("key")
assert.NotNil(t, err)
val, err := client.Scard("key")
assert.Nil(t, err)
assert.Equal(t, int64(4), val)
_, err = NewRedis(client.Addr, "").Sismember("key", 2)
assert.NotNil(t, err)
ok, err := client.Sismember("key", 2)
assert.Nil(t, err)
assert.True(t, ok)
_, err = NewRedis(client.Addr, "").Srem("key", 3, 4)
assert.NotNil(t, err)
num, err = client.Srem("key", 3, 4)
assert.Nil(t, err)
assert.Equal(t, 2, num)
_, err = NewRedis(client.Addr, "").Smembers("key")
assert.NotNil(t, err)
vals, err := client.Smembers("key")
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"1", "2"}, vals)
_, err = NewRedis(client.Addr, "").Srandmember("key", 1)
assert.NotNil(t, err)
members, err := client.Srandmember("key", 1)
assert.Nil(t, err)
assert.Len(t, members, 1)
assert.Contains(t, []string{"1", "2"}, members[0])
_, err = NewRedis(client.Addr, "").Spop("key")
assert.NotNil(t, err)
member, err := client.Spop("key")
assert.Nil(t, err)
assert.Contains(t, []string{"1", "2"}, member)
_, err = NewRedis(client.Addr, "").Smembers("key")
assert.NotNil(t, err)
vals, err = client.Smembers("key")
assert.Nil(t, err)
assert.NotContains(t, vals, member)
_, err = NewRedis(client.Addr, "").Sadd("key1", 1, 2, 3, 4)
assert.NotNil(t, err)
num, err = client.Sadd("key1", 1, 2, 3, 4)
assert.Nil(t, err)
assert.Equal(t, 4, num)
num, err = client.Sadd("key2", 2, 3, 4, 5)
assert.Nil(t, err)
assert.Equal(t, 4, num)
_, err = NewRedis(client.Addr, "").Sunion("key1", "key2")
assert.NotNil(t, err)
vals, err = client.Sunion("key1", "key2")
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"1", "2", "3", "4", "5"}, vals)
_, err = NewRedis(client.Addr, "").Sunionstore("key3", "key1", "key2")
assert.NotNil(t, err)
num, err = client.Sunionstore("key3", "key1", "key2")
assert.Nil(t, err)
assert.Equal(t, 5, num)
_, err = NewRedis(client.Addr, "").Sdiff("key1", "key2")
assert.NotNil(t, err)
vals, err = client.Sdiff("key1", "key2")
assert.Nil(t, err)
assert.EqualValues(t, []string{"1"}, vals)
_, err = NewRedis(client.Addr, "").Sdiffstore("key4", "key1", "key2")
assert.NotNil(t, err)
num, err = client.Sdiffstore("key4", "key1", "key2")
assert.Nil(t, err)
assert.Equal(t, 1, num)
@@ -359,8 +455,12 @@ func TestRedis_Set(t *testing.T) {
func TestRedis_SetGetDel(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := client.Set("hello", "world")
err := NewRedis(client.Addr, "").Set("hello", "world")
assert.NotNil(t, err)
err = client.Set("hello", "world")
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").Get("hello")
assert.NotNil(t, err)
val, err := client.Get("hello")
assert.Nil(t, err)
assert.Equal(t, "world", val)
@@ -372,8 +472,12 @@ func TestRedis_SetGetDel(t *testing.T) {
func TestRedis_SetExNx(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := client.Setex("hello", "world", 5)
err := NewRedis(client.Addr, "").Setex("hello", "world", 5)
assert.NotNil(t, err)
err = client.Setex("hello", "world", 5)
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").Setnx("hello", "newworld")
assert.NotNil(t, err)
ok, err := client.Setnx("hello", "newworld")
assert.Nil(t, err)
assert.False(t, ok)
@@ -389,6 +493,8 @@ func TestRedis_SetExNx(t *testing.T) {
ttl, err := client.Ttl("hello")
assert.Nil(t, err)
assert.True(t, ttl > 0)
_, err = NewRedis(client.Addr, "").SetnxEx("newhello", "newworld", 5)
assert.NotNil(t, err)
ok, err = client.SetnxEx("newhello", "newworld", 5)
assert.Nil(t, err)
assert.False(t, ok)
@@ -408,12 +514,18 @@ func TestRedis_SetGetDelHashField(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := client.Hset("key", "field", "value")
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").Hget("key", "field")
assert.NotNil(t, err)
val, err := client.Hget("key", "field")
assert.Nil(t, err)
assert.Equal(t, "value", val)
_, err = NewRedis(client.Addr, "").Hexists("key", "field")
assert.NotNil(t, err)
ok, err := client.Hexists("key", "field")
assert.Nil(t, err)
assert.True(t, ok)
_, err = NewRedis(client.Addr, "").Hdel("key", "field")
assert.NotNil(t, err)
ret, err := client.Hdel("key", "field")
assert.Nil(t, err)
assert.True(t, ret)
@@ -434,23 +546,50 @@ func TestRedis_SortedSet(t *testing.T) {
val, err := client.Zscore("key", "value1")
assert.Nil(t, err)
assert.Equal(t, int64(2), val)
_, err = NewRedis(client.Addr, "").Zincrby("key", 3, "value1")
assert.NotNil(t, err)
val, err = client.Zincrby("key", 3, "value1")
assert.Nil(t, err)
assert.Equal(t, int64(5), val)
_, err = NewRedis(client.Addr, "").Zscore("key", "value1")
assert.NotNil(t, err)
val, err = client.Zscore("key", "value1")
assert.Nil(t, err)
assert.Equal(t, int64(5), val)
ok, err = client.Zadd("key", 6, "value2")
val, err = NewRedis(client.Addr, "").Zadds("key")
assert.NotNil(t, err)
val, err = client.Zadds("key", Pair{
Key: "value2",
Score: 6,
}, Pair{
Key: "value3",
Score: 7,
})
assert.Nil(t, err)
assert.True(t, ok)
ok, err = client.Zadd("key", 7, "value3")
assert.Equal(t, int64(2), val)
pairs, err := NewRedis(client.Addr, "").ZRevRangeWithScores("key", 1, 3)
assert.NotNil(t, err)
pairs, err = client.ZRevRangeWithScores("key", 1, 3)
assert.Nil(t, err)
assert.True(t, ok)
assert.EqualValues(t, []Pair{
{
Key: "value2",
Score: 6,
},
{
Key: "value1",
Score: 5,
},
}, pairs)
rank, err := client.Zrank("key", "value2")
assert.Nil(t, err)
assert.Equal(t, int64(1), rank)
_, err = NewRedis(client.Addr, "").Zrank("key", "value4")
assert.NotNil(t, err)
_, err = client.Zrank("key", "value4")
assert.Equal(t, Nil, err)
_, err = NewRedis(client.Addr, "").Zrem("key", "value2", "value3")
assert.NotNil(t, err)
num, err := client.Zrem("key", "value2", "value3")
assert.Nil(t, err)
assert.Equal(t, 2, num)
@@ -463,31 +602,47 @@ func TestRedis_SortedSet(t *testing.T) {
ok, err = client.Zadd("key", 8, "value4")
assert.Nil(t, err)
assert.True(t, ok)
_, err = NewRedis(client.Addr, "").Zremrangebyscore("key", 6, 7)
assert.NotNil(t, err)
num, err = client.Zremrangebyscore("key", 6, 7)
assert.Nil(t, err)
assert.Equal(t, 2, num)
ok, err = client.Zadd("key", 6, "value2")
assert.Nil(t, err)
assert.True(t, ok)
_, err = NewRedis(client.Addr, "").Zadd("key", 7, "value3")
assert.NotNil(t, err)
ok, err = client.Zadd("key", 7, "value3")
assert.Nil(t, err)
assert.True(t, ok)
_, err = NewRedis(client.Addr, "").Zcount("key", 6, 7)
assert.NotNil(t, err)
num, err = client.Zcount("key", 6, 7)
assert.Nil(t, err)
assert.Equal(t, 2, num)
_, err = NewRedis(client.Addr, "").Zremrangebyrank("key", 1, 2)
assert.NotNil(t, err)
num, err = client.Zremrangebyrank("key", 1, 2)
assert.Nil(t, err)
assert.Equal(t, 2, num)
_, err = NewRedis(client.Addr, "").Zcard("key")
assert.NotNil(t, err)
card, err := client.Zcard("key")
assert.Nil(t, err)
assert.Equal(t, 2, card)
_, err = NewRedis(client.Addr, "").Zrange("key", 0, -1)
assert.NotNil(t, err)
vals, err := client.Zrange("key", 0, -1)
assert.Nil(t, err)
assert.EqualValues(t, []string{"value1", "value4"}, vals)
_, err = NewRedis(client.Addr, "").Zrevrange("key", 0, -1)
assert.NotNil(t, err)
vals, err = client.Zrevrange("key", 0, -1)
assert.Nil(t, err)
assert.EqualValues(t, []string{"value4", "value1"}, vals)
pairs, err := client.ZrangeWithScores("key", 0, -1)
_, err = NewRedis(client.Addr, "").ZrangeWithScores("key", 0, -1)
assert.NotNil(t, err)
pairs, err = client.ZrangeWithScores("key", 0, -1)
assert.Nil(t, err)
assert.EqualValues(t, []Pair{
{
@@ -499,6 +654,8 @@ func TestRedis_SortedSet(t *testing.T) {
Score: 8,
},
}, pairs)
_, err = NewRedis(client.Addr, "").ZrangebyscoreWithScores("key", 5, 8)
assert.NotNil(t, err)
pairs, err = client.ZrangebyscoreWithScores("key", 5, 8)
assert.Nil(t, err)
assert.EqualValues(t, []Pair{
@@ -511,6 +668,9 @@ func TestRedis_SortedSet(t *testing.T) {
Score: 8,
},
}, pairs)
_, err = NewRedis(client.Addr, "").ZrangebyscoreWithScoresAndLimit(
"key", 5, 8, 1, 1)
assert.NotNil(t, err)
pairs, err = client.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
assert.Nil(t, err)
assert.EqualValues(t, []Pair{
@@ -519,6 +679,11 @@ func TestRedis_SortedSet(t *testing.T) {
Score: 8,
},
}, pairs)
pairs, err = client.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
assert.Nil(t, err)
assert.Equal(t, 0, len(pairs))
_, err = NewRedis(client.Addr, "").ZrevrangebyscoreWithScores("key", 5, 8)
assert.NotNil(t, err)
pairs, err = client.ZrevrangebyscoreWithScores("key", 5, 8)
assert.Nil(t, err)
assert.EqualValues(t, []Pair{
@@ -531,6 +696,9 @@ func TestRedis_SortedSet(t *testing.T) {
Score: 5,
},
}, pairs)
_, err = NewRedis(client.Addr, "").ZrevrangebyscoreWithScoresAndLimit(
"key", 5, 8, 1, 1)
assert.NotNil(t, err)
pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
assert.Nil(t, err)
assert.EqualValues(t, []Pair{
@@ -539,11 +707,17 @@ func TestRedis_SortedSet(t *testing.T) {
Score: 5,
},
}, pairs)
pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
assert.Nil(t, err)
assert.Equal(t, 0, len(pairs))
})
}
func TestRedis_Pipelined(t *testing.T) {
runOnRedis(t, func(client *Redis) {
assert.NotNil(t, NewRedis(client.Addr, "").Pipelined(func(pipeliner Pipeliner) error {
return nil
}))
err := client.Pipelined(
func(pipe Pipeliner) error {
pipe.Incr("pipelined_counter")
@@ -553,6 +727,8 @@ func TestRedis_Pipelined(t *testing.T) {
},
)
assert.Nil(t, err)
_, err = NewRedis(client.Addr, "").Ttl("pipelined_counter")
assert.NotNil(t, err)
ttl, err := client.Ttl("pipelined_counter")
assert.Nil(t, err)
assert.Equal(t, 3600, ttl)
@@ -565,6 +741,76 @@ func TestRedis_Pipelined(t *testing.T) {
})
}
func TestRedisString(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
_, err := getRedis(NewRedis(client.Addr, ClusterType))
assert.Nil(t, err)
assert.Equal(t, client.Addr, client.String())
assert.NotNil(t, NewRedis(client.Addr, "").Ping())
})
}
func TestRedisScriptLoad(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
_, err := NewRedis(client.Addr, "").scriptLoad("foo")
assert.NotNil(t, err)
_, err = client.scriptLoad("foo")
assert.NotNil(t, err)
})
}
func TestRedisToPairs(t *testing.T) {
pairs := toPairs([]red.Z{
{
Member: 1,
Score: 1,
},
{
Member: 2,
Score: 2,
},
})
assert.EqualValues(t, []Pair{
{
Key: "1",
Score: 1,
},
{
Key: "2",
Score: 2,
},
}, pairs)
}
func TestRedisToStrings(t *testing.T) {
vals := toStrings([]interface{}{1, 2})
assert.EqualValues(t, []string{"1", "2"}, vals)
}
func TestRedisBlpop(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
var node mockedNode
_, err := client.Blpop(nil, "foo")
assert.NotNil(t, err)
_, err = client.Blpop(node, "foo")
assert.NotNil(t, err)
})
}
func TestRedisBlpopEx(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
var node mockedNode
_, _, err := client.BlpopEx(nil, "foo")
assert.NotNil(t, err)
_, _, err = client.BlpopEx(node, "foo")
assert.NotNil(t, err)
})
}
func runOnRedis(t *testing.T, fn func(client *Redis)) {
s, err := miniredis.Run()
assert.Nil(t, err)
@@ -576,8 +822,18 @@ func runOnRedis(t *testing.T, fn func(client *Redis)) {
t.Error(err)
}
client.Close()
if client != nil {
client.Close()
}
}()
fn(NewRedis(s.Addr(), NodeType))
}
type mockedNode struct {
RedisNode
}
func (n mockedNode) BLPop(timeout time.Duration, keys ...string) *red.StringSliceCmd {
return red.NewStringSliceCmd("foo", "bar")
}

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/internal"
"github.com/tal-tech/go-zero/core/stores/redis"
"github.com/tal-tech/go-zero/core/stores/sqlx"
"github.com/tal-tech/go-zero/core/syncx"
@@ -19,7 +18,7 @@ var (
// can't use one SharedCalls per conn, because multiple conns may share the same cache key.
exclusiveCalls = syncx.NewSharedCalls()
stats = internal.NewCacheStat("sqlc")
stats = cache.NewCacheStat("sqlc")
)
type (
@@ -30,21 +29,21 @@ type (
CachedConn struct {
db sqlx.SqlConn
cache internal.Cache
cache cache.Cache
}
)
func NewNodeConn(db sqlx.SqlConn, rds *redis.Redis, opts ...cache.Option) CachedConn {
return CachedConn{
db: db,
cache: internal.NewCacheNode(rds, exclusiveCalls, stats, sql.ErrNoRows, opts...),
cache: cache.NewCacheNode(rds, exclusiveCalls, stats, sql.ErrNoRows, opts...),
}
}
func NewConn(db sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) CachedConn {
return CachedConn{
db: db,
cache: internal.NewCache(c, exclusiveCalls, stats, sql.ErrNoRows, opts...),
cache: cache.NewCache(c, exclusiveCalls, stats, sql.ErrNoRows, opts...),
}
}
@@ -83,6 +82,7 @@ func (cc CachedConn) QueryRowIndex(v interface{}, key string, keyer func(primary
indexQuery IndexQueryFn, primaryQuery PrimaryQueryFn) error {
var primaryKey interface{}
var found bool
if err := cc.cache.TakeWithExpire(&primaryKey, key, func(val interface{}, expire time.Duration) (err error) {
primaryKey, err = indexQuery(cc.db, v)
if err != nil {

View File

@@ -79,9 +79,29 @@ func TestCachedConn_QueryRowIndex_NoCache(t *testing.T) {
}
r := redis.NewRedis(s.Addr(), redis.NodeType)
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
c := NewConn(dummySqlConn{}, cache.CacheConf{
{
RedisConf: redis.RedisConf{
Host: s.Addr(),
Type: redis.NodeType,
},
Weight: 100,
},
}, cache.WithExpiry(time.Second*10))
var str string
err = c.QueryRowIndex(&str, "index", func(s interface{}) string {
return fmt.Sprintf("%s/1234", s)
}, func(conn sqlx.SqlConn, v interface{}) (interface{}, error) {
*v.(*string) = "zero"
return "primary", errors.New("foo")
}, func(conn sqlx.SqlConn, v, pri interface{}) error {
assert.Equal(t, "primary", pri)
*v.(*string) = "xin"
return nil
})
assert.NotNil(t, err)
err = c.QueryRowIndex(&str, "index", func(s interface{}) string {
return fmt.Sprintf("%s/1234", s)
}, func(conn sqlx.SqlConn, v interface{}) (interface{}, error) {
@@ -135,6 +155,103 @@ func TestCachedConn_QueryRowIndex_HasCache(t *testing.T) {
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 interface{}
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),
},
}
s, err := miniredis.Run()
if err != nil {
t.Error(err)
}
defer s.Close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resetStats()
s.FlushAll()
r := redis.NewRedis(s.Addr(), redis.NodeType)
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 interface{}) string {
return fmt.Sprintf("%v/1234", s)
}, func(conn sqlx.SqlConn, v interface{}) (interface{}, error) {
assert.Fail(t, "should not go here")
return test.primary, nil
}, func(conn sqlx.SqlConn, v, primary interface{}) 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",
@@ -148,6 +265,8 @@ func TestCachedConn_QueryRowIndex_HasWrongCache(t *testing.T) {
if err != nil {
t.Error(err)
}
s.FlushAll()
defer s.Close()
r := redis.NewRedis(s.Addr(), redis.NodeType)
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
@@ -401,6 +520,10 @@ func TestCachedConnExecDropCache(t *testing.T) {
assert.True(t, conn.execValue)
_, err = s.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 TestCachedConnExecDropCacheFailed(t *testing.T) {
@@ -446,6 +569,31 @@ func TestCachedConnTransact(t *testing.T) {
assert.True(t, conn.transactValue)
}
func TestQueryRowNoCache(t *testing.T) {
s, err := miniredis.Run()
if err != nil {
t.Error(err)
}
const (
key = "user"
value = "any"
)
var user string
var ran bool
r := redis.NewRedis(s.Addr(), redis.NodeType)
conn := dummySqlConn{queryRow: func(v interface{}, q string, args ...interface{}) 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 resetStats() {
atomic.StoreUint64(&stats.Total, 0)
atomic.StoreUint64(&stats.Hit, 0)
@@ -454,6 +602,7 @@ func resetStats() {
}
type dummySqlConn struct {
queryRow func(interface{}, string, ...interface{}) error
}
func (d dummySqlConn) Exec(query string, args ...interface{}) (sql.Result, error) {
@@ -465,6 +614,9 @@ func (d dummySqlConn) Prepare(query string) (sqlx.StmtSession, error) {
}
func (d dummySqlConn) QueryRow(v interface{}, query string, args ...interface{}) error {
if d.queryRow != nil {
return d.queryRow(v, query, args...)
}
return nil
}

View File

@@ -2,6 +2,7 @@ package sqlx
import (
"database/sql"
"errors"
"strconv"
"testing"
@@ -11,14 +12,15 @@ import (
)
type mockedConn struct {
query string
args []interface{}
query string
args []interface{}
execErr error
}
func (c *mockedConn) Exec(query string, args ...interface{}) (sql.Result, error) {
c.query = query
c.args = args
return nil, nil
return nil, c.execErr
}
func (c *mockedConn) Prepare(query string) (StmtSession, error) {
@@ -68,9 +70,12 @@ func TestBulkInserterSuffix(t *testing.T) {
inserter, err := NewBulkInserter(&conn, `INSERT INTO classroom_dau(classroom, user, count) VALUES`+
`(?, ?, ?) ON DUPLICATE KEY UPDATE is_overtime=VALUES(is_overtime)`)
assert.Nil(t, err)
assert.Nil(t, inserter.UpdateStmt(`INSERT INTO classroom_dau(classroom, user, count) VALUES`+
`(?, ?, ?) ON DUPLICATE KEY UPDATE is_overtime=VALUES(is_overtime)`))
for i := 0; i < 5; i++ {
assert.Nil(t, inserter.Insert("class_"+strconv.Itoa(i), "user_"+strconv.Itoa(i), i))
}
inserter.SetResultHandler(func(result sql.Result, err error) {})
inserter.Flush()
assert.Equal(t, `INSERT INTO classroom_dau(classroom, user, count) VALUES `+
`('class_0', 'user_0', 0), ('class_1', 'user_1', 1), ('class_2', 'user_2', 2), `+
@@ -80,6 +85,33 @@ func TestBulkInserterSuffix(t *testing.T) {
})
}
func TestBulkInserterBadStatement(t *testing.T) {
runSqlTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
var conn mockedConn
_, err := NewBulkInserter(&conn, "foo")
assert.NotNil(t, err)
})
}
func TestBulkInserter_Update(t *testing.T) {
conn := mockedConn{
execErr: errors.New("foo"),
}
_, err := NewBulkInserter(&conn, `INSERT INTO classroom_dau(classroom, user, count) VALUES()`)
assert.NotNil(t, err)
_, err = NewBulkInserter(&conn, `INSERT INTO classroom_dau(classroom, user, count) VALUES(?)`)
assert.NotNil(t, err)
inserter, err := NewBulkInserter(&conn, `INSERT INTO classroom_dau(classroom, user, count) VALUES(?, ?, ?)`)
assert.Nil(t, err)
inserter.inserter.Execute([]string{"bar"})
inserter.SetResultHandler(func(result sql.Result, err error) {
})
inserter.UpdateOrDelete(func() {})
inserter.inserter.Execute([]string(nil))
assert.NotNil(t, inserter.UpdateStmt("foo"))
assert.NotNil(t, inserter.Insert("foo", "bar"))
}
func runSqlTest(t *testing.T, fn func(db *sql.DB, mock sqlmock.Sqlmock)) {
logx.Disable()

View File

@@ -2,6 +2,7 @@ package sqlx
import (
"database/sql"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
@@ -22,6 +23,18 @@ func TestUnmarshalRowBool(t *testing.T) {
})
}
func TestUnmarshalRowBoolNotSettable(t *testing.T) {
runOrmTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
rs := sqlmock.NewRows([]string{"value"}).FromCSVString("1")
mock.ExpectQuery("select (.+) from users where user=?").WithArgs("anyone").WillReturnRows(rs)
var value bool
assert.NotNil(t, query(db, func(rows *sql.Rows) error {
return unmarshalRow(value, rows, true)
}, "select value from users where user=?", "anyone"))
})
}
func TestUnmarshalRowInt(t *testing.T) {
runOrmTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
rs := sqlmock.NewRows([]string{"value"}).FromCSVString("2")
@@ -228,6 +241,22 @@ func TestUnmarshalRowStructWithTags(t *testing.T) {
})
}
func TestUnmarshalRowStructWithTagsWrongColumns(t *testing.T) {
var value = new(struct {
Age *int `db:"age"`
Name string `db:"name"`
})
runOrmTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
rs := sqlmock.NewRows([]string{"name"}).FromCSVString("liao")
mock.ExpectQuery("select (.+) from users where user=?").WithArgs("anyone").WillReturnRows(rs)
assert.NotNil(t, query(db, func(rows *sql.Rows) error {
return unmarshalRow(value, rows, true)
}, "select name, age from users where user=?", "anyone"))
})
}
func TestUnmarshalRowsBool(t *testing.T) {
runOrmTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
var expect = []bool{true, false}
@@ -955,6 +984,62 @@ func TestCommonSqlConn_QueryRowOptional(t *testing.T) {
})
}
func TestUnmarshalRowError(t *testing.T) {
tests := []struct {
name string
colErr error
scanErr error
err error
next int
validate func(err error)
}{
{
name: "with error",
err: errors.New("foo"),
validate: func(err error) {
assert.NotNil(t, err)
},
},
{
name: "without next",
validate: func(err error) {
assert.Equal(t, ErrNotFound, err)
},
},
{
name: "with error",
scanErr: errors.New("foo"),
next: 1,
validate: func(err error) {
assert.Equal(t, ErrNotFound, err)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
runOrmTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
rs := sqlmock.NewRows([]string{"age"}).FromCSVString("5")
mock.ExpectQuery("select (.+) from users where user=?").WithArgs(
"anyone").WillReturnRows(rs)
var r struct {
User string `db:"user"`
Age int `db:"age"`
}
test.validate(query(db, func(rows *sql.Rows) error {
scanner := mockedScanner{
colErr: test.colErr,
scanErr: test.scanErr,
err: test.err,
}
return unmarshalRow(&r, &scanner, false)
}, "select age from users where user=?", "anyone"))
})
})
}
}
func runOrmTest(t *testing.T, fn func(db *sql.DB, mock sqlmock.Sqlmock)) {
logx.Disable()
@@ -970,3 +1055,30 @@ func runOrmTest(t *testing.T, fn func(db *sql.DB, mock sqlmock.Sqlmock)) {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
type mockedScanner struct {
colErr error
scanErr error
err error
next int
}
func (m *mockedScanner) Columns() ([]string, error) {
return nil, m.colErr
}
func (m *mockedScanner) Err() error {
return m.err
}
func (m *mockedScanner) Next() bool {
if m.next > 0 {
m.next--
return true
}
return false
}
func (m *mockedScanner) Scan(v ...interface{}) error {
return m.scanErr
}

View File

@@ -1,6 +1,7 @@
package syncx
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -10,11 +11,15 @@ func TestBarrier_Guard(t *testing.T) {
const total = 10000
var barrier Barrier
var count int
var wg sync.WaitGroup
wg.Add(total)
for i := 0; i < total; i++ {
barrier.Guard(func() {
go barrier.Guard(func() {
count++
wg.Done()
})
}
wg.Wait()
assert.Equal(t, total, count)
}
@@ -22,10 +27,14 @@ func TestBarrierPtr_Guard(t *testing.T) {
const total = 10000
barrier := new(Barrier)
var count int
wg := new(sync.WaitGroup)
wg.Add(total)
for i := 0; i < total; i++ {
barrier.Guard(func() {
go barrier.Guard(func() {
count++
wg.Done()
})
}
wg.Wait()
assert.Equal(t, total, count)
}

View File

@@ -67,7 +67,3 @@ func TestSignalNoWait(t *testing.T) {
func sleep(millisecond int) {
time.Sleep(time.Duration(millisecond) * time.Millisecond)
}
func currentTimeMillis() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}

View File

@@ -6,18 +6,22 @@ import (
"github.com/tal-tech/go-zero/core/lang"
)
var ErrReturn = errors.New("discarding limited token, resource pool is full, someone returned multiple times")
// ErrLimitReturn indicates that the more than borrowed elements were returned.
var ErrLimitReturn = errors.New("discarding limited token, resource pool is full, someone returned multiple times")
// Limit controls the concurrent requests.
type Limit struct {
pool chan lang.PlaceholderType
}
// NewLimit creates a Limit that can borrow n elements from it concurrently.
func NewLimit(n int) Limit {
return Limit{
pool: make(chan lang.PlaceholderType, n),
}
}
// Borrow borrows an element from Limit in blocking mode.
func (l Limit) Borrow() {
l.pool <- lang.Placeholder
}
@@ -28,10 +32,12 @@ func (l Limit) Return() error {
case <-l.pool:
return nil
default:
return ErrReturn
return ErrLimitReturn
}
}
// TryBorrow tries to borrow an element from Limit, in non-blocking mode.
// If success, true returned, false for otherwise.
func (l Limit) TryBorrow() bool {
select {
case l.pool <- lang.Placeholder:

View File

@@ -13,5 +13,5 @@ func TestLimit(t *testing.T) {
assert.False(t, limit.TryBorrow())
assert.Nil(t, limit.Return())
assert.Nil(t, limit.Return())
assert.Equal(t, ErrReturn, limit.Return())
assert.Equal(t, ErrLimitReturn, limit.Return())
}

View File

@@ -33,35 +33,42 @@ func NewSharedCalls() SharedCalls {
}
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.lock.Lock()
if c, ok := g.calls[key]; ok {
g.lock.Unlock()
c.wg.Wait()
c, done := g.createCall(key, fn)
if done {
return c.val, c.err
}
c := g.makeCall(key, fn)
g.makeCall(c, key, fn)
return c.val, c.err
}
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
c, done := g.createCall(key, fn)
if done {
return c.val, false, c.err
}
g.makeCall(c, key, fn)
return c.val, true, c.err
}
func (g *sharedGroup) createCall(key string, fn func() (interface{}, error)) (c *call, done bool) {
g.lock.Lock()
if c, ok := g.calls[key]; ok {
g.lock.Unlock()
c.wg.Wait()
return c.val, false, c.err
return c, true
}
c := g.makeCall(key, fn)
return c.val, true, c.err
}
func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call {
c := new(call)
c = new(call)
c.wg.Add(1)
g.calls[key] = c
g.lock.Unlock()
return c, false
}
func (g *sharedGroup) makeCall(c *call, key string, fn func() (interface{}, error)) {
defer func() {
// delete key first, done later. can't reverse the order, because if reverse,
// another Do call might wg.Wait() without get notified with wg.Done()
@@ -72,5 +79,4 @@ func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call
}()
c.val, c.err = fn()
return c
}

View File

@@ -95,7 +95,8 @@ func TestExclusiveCallDoDiffDupSuppress(t *testing.T) {
close(broadcast)
wg.Wait()
if got := atomic.LoadInt32(&calls); got != 5 { // five letters
if got := atomic.LoadInt32(&calls); got != 5 {
// five letters
t.Errorf("number of calls = %d; want 5", got)
}
}

View File

@@ -29,5 +29,5 @@ func TestTimeoutLimit(t *testing.T) {
assert.Equal(t, ErrTimeout, limit.Borrow(time.Millisecond*100))
assert.Nil(t, limit.Return())
assert.Nil(t, limit.Return())
assert.Equal(t, ErrReturn, limit.Return())
assert.Equal(t, ErrLimitReturn, limit.Return())
}

View File

@@ -16,7 +16,7 @@ func TestRelativeTime(t *testing.T) {
}
func TestRelativeTime_Time(t *testing.T) {
diff := Time().Sub(time.Now())
diff := time.Until(Time())
if diff > 0 {
assert.True(t, diff < time.Second)
} else {

View File

@@ -27,12 +27,9 @@ func TestFakeTicker(t *testing.T) {
var count int32
go func() {
for {
select {
case <-ticker.Chan():
if atomic.AddInt32(&count, 1) == total {
ticker.Done()
}
for range ticker.Chan() {
if atomic.AddInt32(&count, 1) == total {
ticker.Done()
}
}
}()

View File

@@ -18,7 +18,7 @@ func TestHttpPropagator_Extract(t *testing.T) {
assert.Equal(t, "trace", carrier.Get(traceIdKey))
assert.Equal(t, "span", carrier.Get(spanIdKey))
carrier, err = Extract(HttpFormat, req)
_, err = Extract(HttpFormat, req)
assert.Equal(t, ErrInvalidCarrier, err)
}
@@ -31,7 +31,7 @@ func TestHttpPropagator_Inject(t *testing.T) {
assert.Equal(t, "trace", carrier.Get(traceIdKey))
assert.Equal(t, "span", carrier.Get(spanIdKey))
carrier, err = Inject(HttpFormat, req)
_, err = Inject(HttpFormat, req)
assert.Equal(t, ErrInvalidCarrier, err)
}
@@ -45,9 +45,9 @@ func TestGrpcPropagator_Extract(t *testing.T) {
assert.Equal(t, "trace", carrier.Get(traceIdKey))
assert.Equal(t, "span", carrier.Get(spanIdKey))
carrier, err = Extract(GrpcFormat, 1)
_, err = Extract(GrpcFormat, 1)
assert.Equal(t, ErrInvalidCarrier, err)
carrier, err = Extract(nil, 1)
_, err = Extract(nil, 1)
assert.Equal(t, ErrInvalidCarrier, err)
}
@@ -61,8 +61,8 @@ func TestGrpcPropagator_Inject(t *testing.T) {
assert.Equal(t, "trace", carrier.Get(traceIdKey))
assert.Equal(t, "span", carrier.Get(spanIdKey))
carrier, err = Inject(GrpcFormat, 1)
_, err = Inject(GrpcFormat, 1)
assert.Equal(t, ErrInvalidCarrier, err)
carrier, err = Inject(nil, 1)
_, err = Inject(nil, 1)
assert.Equal(t, ErrInvalidCarrier, err)
}

View File

@@ -17,7 +17,6 @@ const (
clientFlag = "client"
serverFlag = "server"
spanSepRune = '.'
timeFormat = "2006-01-02 15:04:05.000"
)
var spanSep = string([]byte{spanSepRune})
@@ -37,9 +36,7 @@ func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec
return carrier.Get(traceIdKey)
}
return ""
}, func() string {
return stringx.RandId()
})
}, stringx.RandId)
spanId := stringx.TakeWithPriority(func() string {
if carrier != nil {
return carrier.Get(spanIdKey)

View File

@@ -30,6 +30,7 @@ func TestCompareVersions(t *testing.T) {
}
for _, each := range cases {
each := each
t.Run(each.ver1, func(t *testing.T) {
actual := CompareVersions(each.ver1, each.operator, each.ver2)
assert.Equal(t, each.out, actual, fmt.Sprintf("%s vs %s", each.ver1, each.ver2))

View File

@@ -1,622 +0,0 @@
# Rapid development of microservices - multiple RPCs
English | [简体中文](bookstore.md)
## 0. Why building microservices are so difficult
To build a well working microservice, we need lots of knowledges from different aspects.
* basic functionalities
1. concurrency control and rate limit, to avoid being brought down by unexpected inbound
2. service discovery, make sure new or terminated nodes are detected asap
3. load balancing, balance the traffic base on the throughput of nodes
4. timeout control, avoid the nodes continue to process the timed out requests
5. circuit breaker, load shedding, fail fast, protects the failure nodes to recover asap
* advanced functionalities
1. authorization, make sure users can only access their own data
2. tracing, to understand the whole system and locate the specific problem quickly
3. logging, collects data and helps to backtrace problems
4. observability, no metrics, no optimization
For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, its very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/tal-tech/go-zero) is born for this purpose, especially for cloud-native microservice systems.
As well, we always adhere to the idea that **prefer tools over conventions and documents**. We hope to reduce the boilerplate code as much as possible, and let developers focus on developing the business related code. For this purpose, we developed the tool `goctl`.
Lets take the shorturl microservice as a quick example to demonstrate how to quickly create microservices by using [go-zero](https://github.com/tal-tech/go-zero). After finishing this tutorial, youll find that its so easy to write microservices!
## 1. What is a bookstore service
For simplicity, the bookstore service only contains two functionalities, adding books and quering prices.
Writting this bookstore service is to demonstrate the complete flow of creating a microservice by using go-zero. But algorithms and detail implementations are quite simplified, and this bookstore service is not suitable for production use.
## 2. Architecture of shorturl microservice
<img src="images/bookstore-arch.png" alt="architecture" width="800" />
## 3. goctl generated code overview
All modules with green background are generated, and will be enabled when necessary. The modules with red background are handwritten code, which is typically business logic code.
* API Gateway
<img src="images/api-gen.png" alt="api" width="800" />
* RPC
<img src="images/rpc-gen.png" alt="rpc" width="800" />
* model
<img src="images/model-gen.png" alt="model" width="800" />
And now, lets walk through the complete flow of quickly create a microservice with go-zero.
## 4. Get started
* install etcd, mysql, redis
* install protoc-gen-go
```shell
go get -u github.com/golang/protobuf/protoc-gen-go
```
* install goctl
```shell
GO111MODULE=on go get -u github.com/tal-tech/go-zero/tools/goctl
```
* create the working dir bookstore
* in `bookstore` dir, execute `go mod init bookstore` to initialize `go.mod``
## 5. Write code for API Gateway
* use goctl to generate `api/bookstore.api`
```Plain Text
goctl api -o bookstore.api
```
for simplicity, the leading `info` block is removed, and the code looks like:
```go
type (
addReq struct {
book string `form:"book"`
price int64 `form:"price"`
}
addResp struct {
ok bool `json:"ok"`
}
)
type (
checkReq struct {
book string `form:"book"`
}
checkResp struct {
found bool `json:"found"`
price int64 `json:"price"`
}
)
service bookstore-api {
@server(
handler: AddHandler
)
get /add(addReq) returns(addResp)
@server(
handler: CheckHandler
)
get /check(checkReq) returns(checkResp)
}
```
the usage of `type` keyword is the same as that in go, service is used to define get/post/head/delete api requests, described below:
* `service bookstore-api { defines the service name
* `@server` defines the properties that used in server side
* `handler` defines the handler name
* `get /add(addReq) returns(addResp)` defines this is a GET request, the request parameters, and the response parameters
* generate the code for API Gateway by using goctl
```shell
goctl api go -api bookstore.api -dir .
```
the generated file structure looks like:
```Plain Text
api
├── bookstore.api // api definition
├── bookstore.go // main entrance
├── etc
│ └── bookstore-api.yaml // configuration file
└── internal
├── config
│ └── config.go // configuration definition
├── handler
│ ├── addhandler.go // implements addHandler
│ ├── checkhandler.go // implements checkHandler
│ └── routes.go // routes definition
├── logic
│ ├── addlogic.go // implements AddLogic
│ └── checklogic.go // implements CheckLogic
├── svc
│ └── servicecontext.go // defines ServiceContext
└── types
└── types.go // defines request/response
```
* start API Gateway service, listens on port 8888 by default
```shell
go run bookstore.go -f etc/bookstore-api.yaml
```
* test API Gateway service
```shell
curl -i "http://localhost:8888/check?book=go-zero"
```
response like:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 06:46:18 GMT
Content-Length: 25
{"found":false,"price":0}
```
You can see that the API Gateway service did nothing except returned a zero value. And lets implement the business logic in rpc service.
* you can modify `internal/svc/servicecontext.go` to pass dependencies if needed
* implement logic in package `internal/logic`
* you can use goctl to generate code for clients base on the .api file
* till now, the client engineer can work with the api, dont need to wait for the implementation of server side
## 6. Write code for add rpc service
* under directory `rpc/add` create `add.proto` file
```shell
goctl rpc template -o add.proto
```
edit the file and make the code looks like:
```protobuf
syntax = "proto3";
package add;
message addReq {
string book = 1;
int64 price = 2;
}
message addResp {
bool ok = 1;
}
service adder {
rpc add(addReq) returns(addResp);
}
```
* use goctl to generate the rpc code, execute the following command in `rpc/add`
```shell
goctl rpc proto -src add.proto
```
the generated file structure looks like:
```Plain Text
rpc/add
├── add.go // rpc main entrance
├── add.proto // rpc definition
├── adder
│ ├── adder.go // defines how rpc clients call this service
│ ├── adder_mock.go // mock file, for test purpose
│ └── types.go // request/response definition
├── etc
│ └── add.yaml // configuration file
├── internal
│ ├── config
│ │ └── config.go // configuration definition
│ ├── logic
│ │ └── addlogic.go // add logic here
│ ├── server
│ │ └── adderserver.go // rpc handler
│ └── svc
│ └── servicecontext.go // defines service context, like dependencies
└── pb
└── add.pb.go
```
just run it, looks like:
```shell
$ go run add.go -f etc/add.yaml
Starting rpc server at 127.0.0.1:8080...
```
you can change the listening port in file `etc/add.yaml`.
## 7. Write code for check rpc service
* under directory `rpc/check` create `check.proto` file
```shell
goctl rpc template -o check.proto
```
edit the file and make the code looks like:
```protobuf
syntax = "proto3";
package check;
message checkReq {
string book = 1;
}
message checkResp {
bool found = 1;
int64 price = 2;
}
service checker {
rpc check(checkReq) returns(checkResp);
}
```
* use goctl to generate the rpc code, execute the following command in `rpc/check`
```shell
goctl rpc proto -src check.proto
```
the generated file structure looks like:
```Plain Text
rpc/check
├── check.go // rpc main entrance
├── check.proto // rpc definition
├── checker
│ ├── checker.go // defines how rpc clients call this service
│ ├── checker_mock.go // mock file, for test purpose
│ └── types.go // request/response definition
├── etc
│ └── check.yaml // configuration file
├── internal
│ ├── config
│ │ └── config.go // configuration definition
│ ├── logic
│ │ └── checklogic.go // check logic here
│ ├── server
│ │ └── checkerserver.go // rpc handler
│ └── svc
│ └── servicecontext.go // defines service context, like dependencies
└── pb
└── check.pb.go
```
you can change the listening port in `etc/check.yaml`.
we need to change the port in `etc/check.yaml` to `8081`, because `8080` is used by `add` service.
just run it, looks like:
```shell
$ go run check.go -f etc/check.yaml
Starting rpc server at 127.0.0.1:8081...
```
## 8. Modify API Gateway to call add/check rpc service
* modify the configuration file `bookstore-api.yaml`, add the following:
```yaml
Add:
Etcd:
Hosts:
- localhost:2379
Key: add.rpc
Check:
Etcd:
Hosts:
- localhost:2379
Key: check.rpc
```
automatically discover the add/check service by using etcd.
* modify the file `internal/config/config.go`, add dependency on add/check service:
```go
type Config struct {
rest.RestConf
Add zrpc.RpcClientConf // manual code
Check zrpc.RpcClientConf // manual code
}
```
* modify the file `internal/svc/servicecontext.go`, like below:
```go
type ServiceContext struct {
Config config.Config
Adder adder.Adder // manual code
Checker checker.Checker // manual code
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Adder: adder.NewAdder(zrpc.MustNewClient(c.Add)), // manual code
Checker: checker.NewChecker(zrpc.MustNewClient(c.Check)), // manual code
}
}
```
passing the dependencies among services within ServiceContext.
* modify the method `Add` in the file `internal/logic/addlogic.go`, looks like:
```go
func (l *AddLogic) Add(req types.AddReq) (*types.AddResp, error) {
// manual code start
resp, err := l.svcCtx.Adder.Add(l.ctx, &adder.AddReq{
Book: req.Book,
Price: req.Price,
})
if err != nil {
return nil, err
}
return &types.AddResp{
Ok: resp.Ok,
}, nil
// manual code stop
}
```
by calling the method `Add` of `adder` to add books into bookstore.
* modify the file `internal/logic/checklogic.go`, looks like:
```go
func (l *CheckLogic) Check(req types.CheckReq) (*types.CheckResp, error) {
// manual code start
resp, err := l.svcCtx.Checker.Check(l.ctx, &checker.CheckReq{
Book: req.Book,
})
if err != nil {
return nil, err
}
return &types.CheckResp{
Found: resp.Found,
Price: resp.Price,
}, nil
// manual code stop
}
```
by calling the method `Check` of `checker` to check the prices from the bookstore.
Till now, weve done the modification of API Gateway. All the manually added code are marked.
## 9. Define the database schema, generate the code for CRUD+cache
* under bookstore, create the directory `rpc/model`: `mkdir -p rpc/model`
* under the directory rpc/model create the file called `book.sql`, contents as below:
```sql
CREATE TABLE `book`
(
`book` varchar(255) NOT NULL COMMENT 'book name',
`price` int NOT NULL COMMENT 'book price',
PRIMARY KEY(`book`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
* create DB and table
```sql
create database gozero;
```
```sql
source book.sql;
```
* under the directory `rpc/model` execute the following command to genrate CRUD+cache code, `-c` means using `redis cache`
```shell
goctl model mysql ddl -c -src book.sql -dir .
```
you can also generate the code from the database url by using `datasource` subcommand instead of `ddl`
the generated file structure looks like:
```Plain Text
rpc/model
├── bookstore.sql
├── bookstoremodel.go // CRUD+cache code
└── vars.go // const and var definition
```
## 10. Modify add/check rpc to call crud+cache
* modify `rpc/add/etc/add.yaml`, add the following:
```yaml
DataSource: root:@tcp(localhost:3306)/gozero
Table: book
Cache:
- Host: localhost:6379
```
you can use multiple redis as cache. redis node and cluster are both supported.
* modify `rpc/add/internal/config.go`, like below:
```go
type Config struct {
zrpc.RpcServerConf
DataSource string // manual code
Table string // manual code
Cache cache.CacheConf // manual code
}
```
added the configuration for mysql and redis cache.
* modify `rpc/add/internal/svc/servicecontext.go` and `rpc/check/internal/svc/servicecontext.go`, like below:
```go
type ServiceContext struct {
c config.Config
Model *model.BookModel // manual code
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
c: c,
Model: model.NewBookModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // manual code
}
}
```
* modify `rpc/add/internal/logic/addlogic.go`, like below:
```go
func (l *AddLogic) Add(in *add.AddReq) (*add.AddResp, error) {
// manual code start
_, err := l.svcCtx.Model.Insert(model.Book{
Book: in.Book,
Price: in.Price,
})
if err != nil {
return nil, err
}
return &add.AddResp{
Ok: true,
}, nil
// manual code stop
}
```
* modify `rpc/check/internal/logic/checklogic.go`, like below:
```go
func (l *CheckLogic) Check(in *check.CheckReq) (*check.CheckResp, error) {
// manual code start
resp, err := l.svcCtx.Model.FindOne(in.Book)
if err != nil {
return nil, err
}
return &check.CheckResp{
Found: true,
Price: resp.Price,
}, nil
// manual code stop
}
```
till now, we finished modifing the code, all the modified code is marked.
## 11. Call shorten and expand services
* call add api
```shell
curl -i "http://localhost:8888/add?book=go-zero&price=10"
```
response like:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 09:42:13 GMT
Content-Length: 11
{"ok":true}
```
* call check api
```shell
curl -i "http://localhost:8888/check?book=go-zero"
```
response like:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 09:47:34 GMT
Content-Length: 25
{"found":true,"price":10}
```
## 12. Benchmark
Because benchmarking the write requests depends on the write throughput of mysql, we only benchmarked the check api. We read the data from mysql and cache it in redis. For simplicity, I only check one book, because of cache, the effect is the same for multiple books.
Before benchmark, we need to change the max open files:
```shel
ulimit -n 20000
```
And change the log level to error, to avoid too many logs affect the benchmark. Add the following in every yaml file:
```yaml
Log:
Level: error
```
![Benchmark](images/bookstore-benchmark.png)
as shown above, in my MacBook Pro, the QPS is like 30K+.
## 13. Full code
[https://github.com/tal-tech/go-zero/tree/master/example/bookstore](https://github.com/tal-tech/go-zero/tree/master/example/bookstore)
## 14. Conclusion
We always adhere to **prefer tools over conventions and documents**.
go-zero is not only a framework, but also a tool to simplify and standardize the building of micoservice systems.
We not only keep the framework simple, but also encapsulate the complexity into the framework. And the developers are free from building the difficult and boilerplate code. Then we get the rapid development and less failure.
For the generated code by goctl, lots of microservice components are included, like concurrency control, adaptive circuit breaker, adaptive load shedding, auto cache control etc. And its easy to deal with the busy sites.
If you have any ideas that can help us to improve the productivity, tell me any time! 👏

View File

@@ -1,622 +0,0 @@
# 快速构建微服务-多RPC版
[English](bookstore-en.md) | 简体中文
## 0. 为什么说做好微服务很难
要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说:
* 基本功能层面
1. 并发控制&限流,避免服务被突发流量击垮
2. 服务注册与服务发现,确保能够动态侦测增减的节点
3. 负载均衡,需要根据节点承受能力分发流量
4. 超时控制,避免对已超时请求做无用功
5. 熔断设计,快速失败,保障故障节点的恢复能力
* 高阶功能层面
1. 请求认证,确保每个用户只能访问自己的数据
2. 链路追踪,用于理解整个系统和快速定位特定请求的问题
3. 日志,用于数据收集和问题定位
4. 可观测性,没有度量就没有优化
对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero微服务框架](https://github.com/tal-tech/go-zero)就是为此而生。
另外,我们始终秉承**工具大于约定和文档**的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了`goctl`工具。
下面我通过书店服务来演示通过[go-zero](https://github.com/tal-tech/go-zero)快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
## 1. 书店服务示例简介
为了教程简单,我们用书店服务做示例,并且只实现其中的增加书目和检查价格功能。
写此书店服务是为了从整体上演示go-zero构建完整微服务的过程实现细节尽可能简化了。
## 2. 书店微服务架构图
<img src="images/bookstore-arch.png" alt="架构图" width="800" />
## 3. goctl各层代码生成一览
所有绿色背景的功能模块是自动生成的,按需激活,红色模块是需要自己写的,也就是增加下依赖,编写业务特有逻辑,各层示意图分别如下:
* API Gateway
<img src="images/bookstore-api.png" alt="api" width="800" />
* RPC
<img src="images/bookstore-rpc.png" alt="架构图" width="800" />
* model
<img src="images/bookstore-model.png" alt="model" width="800" />
下面我们来一起完整走一遍快速构建微服务的流程Lets `Go`!🏃‍♂️
## 4. 准备工作
* 安装etcd, mysql, redis
* 安装`protoc-gen-go`
```shell
go get -u github.com/golang/protobuf/protoc-gen-go
```
* 安装goctl工具
```shell
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
```
* 创建工作目录`bookstore`
* 在`bookstore`目录下执行`go mod init bookstore`初始化`go.mod`
## 5. 编写API Gateway代码
* 在`bookstore/api`目录下通过goctl生成`api/bookstore.api`
```bash
goctl api -o bookstore.api
```
编辑`bookstore.api`,为了简洁,去除了文件开头的`info`,代码如下:
```go
type (
addReq struct {
book string `form:"book"`
price int64 `form:"price"`
}
addResp struct {
ok bool `json:"ok"`
}
)
type (
checkReq struct {
book string `form:"book"`
}
checkResp struct {
found bool `json:"found"`
price int64 `json:"price"`
}
)
service bookstore-api {
@server(
handler: AddHandler
)
get /add(addReq) returns(addResp)
@server(
handler: CheckHandler
)
get /check(checkReq) returns(checkResp)
}
```
type用法和go一致service用来定义get/post/head/delete等api请求解释如下
* `service bookstore-api {`这一行定义了service名字
* `@server`部分用来定义server端用到的属性
* `handler`定义了服务端handler名字
* `get /add(addReq) returns(addResp)`定义了get方法的路由、请求参数、返回参数等
* 使用goctl生成API Gateway代码
```shell
goctl api go -api bookstore.api -dir .
```
生成的文件结构如下:
```Plain Text
api
├── bookstore.api // api定义
├── bookstore.go // main入口定义
├── etc
│ └── bookstore-api.yaml // 配置文件
└── internal
├── config
│ └── config.go // 定义配置
├── handler
│ ├── addhandler.go // 实现addHandler
│ ├── checkhandler.go // 实现checkHandler
│ └── routes.go // 定义路由处理
├── logic
│ ├── addlogic.go // 实现AddLogic
│ └── checklogic.go // 实现CheckLogic
├── svc
│ └── servicecontext.go // 定义ServiceContext
└── types
└── types.go // 定义请求、返回结构体
```
* 启动API Gateway服务默认侦听在8888端口
```shell
go run bookstore.go -f etc/bookstore-api.yaml
```
* 测试API Gateway服务
```shell
curl -i "http://localhost:8888/check?book=go-zero"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 06:46:18 GMT
Content-Length: 25
{"found":false,"price":0}
```
可以看到我们API Gateway其实啥也没干就返回了个空值接下来我们会在rpc服务里实现业务逻辑
* 可以修改`internal/svc/servicecontext.go`来传递服务依赖(如果需要)
* 实现逻辑可以修改`internal/logic`下的对应文件
* 可以通过`goctl`生成各种客户端语言的api调用代码
* 到这里你已经可以通过goctl生成客户端代码给客户端同学并行开发了支持多种语言详见文档
## 6. 编写add rpc服务
* 在`rpc/add`目录下编写`add.proto`文件
可以通过命令生成proto文件模板
```shell
goctl rpc template -o add.proto
```
修改后文件内容如下:
```protobuf
syntax = "proto3";
package add;
message addReq {
string book = 1;
int64 price = 2;
}
message addResp {
bool ok = 1;
}
service adder {
rpc add(addReq) returns(addResp);
}
```
* 用`goctl`生成rpc代码在`rpc/add`目录下执行命令
```shell
goctl rpc proto -src add.proto
```
文件结构如下:
```Plain Text
rpc/add
├── add.go // rpc服务main函数
├── add.proto // rpc接口定义
├── adder
│ ├── adder.go // 提供了外部调用方法,无需修改
│ ├── adder_mock.go // mock方法测试用
│ └── types.go // request/response结构体定义
├── etc
│ └── add.yaml // 配置文件
├── internal
│ ├── config
│ │ └── config.go // 配置定义
│ ├── logic
│ │ └── addlogic.go // add业务逻辑在这里实现
│ ├── server
│ │ └── adderserver.go // 调用入口, 不需要修改
│ └── svc
│ └── servicecontext.go // 定义ServiceContext传递依赖
└── pb
└── add.pb.go
```
直接可以运行,如下:
```shell
$ go run add.go -f etc/add.yaml
Starting rpc server at 127.0.0.1:8080...
```
`etc/add.yaml`文件里可以修改侦听端口等配置
## 7. 编写check rpc服务
* 在`rpc/check`目录下编写`check.proto`文件
可以通过命令生成proto文件模板
```shell
goctl rpc template -o check.proto
```
修改后文件内容如下:
```protobuf
syntax = "proto3";
package check;
message checkReq {
string book = 1;
}
message checkResp {
bool found = 1;
int64 price = 2;
}
service checker {
rpc check(checkReq) returns(checkResp);
}
```
* 用`goctl`生成rpc代码在`rpc/check`目录下执行命令
```shell
goctl rpc proto -src check.proto
```
文件结构如下:
```Plain Text
rpc/check
├── check.go // rpc服务main函数
├── check.proto // rpc接口定义
├── checker
│ ├── checker.go // 提供了外部调用方法,无需修改
│ ├── checker_mock.go // mock方法测试用
│ └── types.go // request/response结构体定义
├── etc
│ └── check.yaml // 配置文件
├── internal
│ ├── config
│ │ └── config.go // 配置定义
│ ├── logic
│ │ └── checklogic.go // check业务逻辑在这里实现
│ ├── server
│ │ └── checkerserver.go // 调用入口, 不需要修改
│ └── svc
│ └── servicecontext.go // 定义ServiceContext传递依赖
└── pb
└── check.pb.go
```
`etc/check.yaml`文件里可以修改侦听端口等配置
需要修改`etc/check.yaml`的端口为`8081`,因为`8080`已经被`add`服务使用了,直接可以运行,如下:
```shell
$ go run check.go -f etc/check.yaml
Starting rpc server at 127.0.0.1:8081...
```
## 8. 修改API Gateway代码调用add/check rpc服务
* 修改配置文件`bookstore-api.yaml`,增加如下内容
```yaml
Add:
Etcd:
Hosts:
- localhost:2379
Key: add.rpc
Check:
Etcd:
Hosts:
- localhost:2379
Key: check.rpc
```
通过etcd自动去发现可用的add/check服务
* 修改`internal/config/config.go`如下增加add/check服务依赖
```go
type Config struct {
rest.RestConf
Add zrpc.RpcClientConf // 手动代码
Check zrpc.RpcClientConf // 手动代码
}
```
* 修改`internal/svc/servicecontext.go`,如下:
```go
type ServiceContext struct {
Config config.Config
Adder adder.Adder // 手动代码
Checker checker.Checker // 手动代码
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Adder: adder.NewAdder(zrpc.MustNewClient(c.Add)), // 手动代码
Checker: checker.NewChecker(zrpc.MustNewClient(c.Check)), // 手动代码
}
}
```
通过ServiceContext在不同业务逻辑之间传递依赖
* 修改`internal/logic/addlogic.go`里的`Add`方法,如下:
```go
func (l *AddLogic) Add(req types.AddReq) (*types.AddResp, error) {
// 手动代码开始
resp, err := l.svcCtx.Adder.Add(l.ctx, &adder.AddReq{
Book: req.Book,
Price: req.Price,
})
if err != nil {
return nil, err
}
return &types.AddResp{
Ok: resp.Ok,
}, nil
// 手动代码结束
}
```
通过调用`adder`的`Add`方法实现添加图书到bookstore系统
* 修改`internal/logic/checklogic.go`里的`Check`方法,如下:
```go
func (l *CheckLogic) Check(req types.CheckReq) (*types.CheckResp, error) {
// 手动代码开始
resp, err := l.svcCtx.Checker.Check(l.ctx, &checker.CheckReq{
Book: req.Book,
})
if err != nil {
return nil, err
}
return &types.CheckResp{
Found: resp.Found,
Price: resp.Price,
}, nil
// 手动代码结束
}
```
通过调用`checker`的`Check`方法实现从bookstore系统中查询图书的价格
## 9. 定义数据库表结构并生成CRUD+cache代码
* bookstore下创建`rpc/model`目录:`mkdir -p rpc/model`
* 在rpc/model目录下编写创建book表的sql文件`book.sql`,如下:
```sql
CREATE TABLE `book`
(
`book` varchar(255) NOT NULL COMMENT 'book name',
`price` int NOT NULL COMMENT 'book price',
PRIMARY KEY(`book`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
* 创建DB和table
```sql
create database gozero;
```
```sql
source book.sql;
```
* 在`rpc/model`目录下执行如下命令生成CRUD+cache代码`-c`表示使用`redis cache`
```shell
goctl model mysql ddl -c -src book.sql -dir .
```
也可以用`datasource`命令代替`ddl`来指定数据库链接直接从schema生成
生成后的文件结构如下:
```Plain Text
rpc/model
├── bookstore.sql
├── bookstoremodel.go // CRUD+cache代码
└── vars.go // 定义常量和变量
```
## 10. 修改add/check rpc代码调用crud+cache代码
* 修改`rpc/add/etc/add.yaml`和`rpc/check/etc/check.yaml`,增加如下内容:
```yaml
DataSource: root:@tcp(localhost:3306)/gozero
Table: book
Cache:
- Host: localhost:6379
```
可以使用多个redis作为cache支持redis单点或者redis集群
* 修改`rpc/add/internal/config.go`和`rpc/check/internal/config.go`,如下:
```go
type Config struct {
zrpc.RpcServerConf
DataSource string // 手动代码
Table string // 手动代码
Cache cache.CacheConf // 手动代码
}
```
增加了mysql和redis cache配置
* 修改`rpc/add/internal/svc/servicecontext.go`和`rpc/check/internal/svc/servicecontext.go`,如下:
```go
type ServiceContext struct {
c config.Config
Model *model.BookModel // 手动代码
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
c: c,
Model: model.NewBookModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手动代码
}
}
```
* 修改`rpc/add/internal/logic/addlogic.go`,如下:
```go
func (l *AddLogic) Add(in *add.AddReq) (*add.AddResp, error) {
// 手动代码开始
_, err := l.svcCtx.Model.Insert(model.Book{
Book: in.Book,
Price: in.Price,
})
if err != nil {
return nil, err
}
return &add.AddResp{
Ok: true,
}, nil
// 手动代码结束
}
```
* 修改`rpc/check/internal/logic/checklogic.go`,如下:
```go
func (l *CheckLogic) Check(in *check.CheckReq) (*check.CheckResp, error) {
// 手动代码开始
resp, err := l.svcCtx.Model.FindOne(in.Book)
if err != nil {
return nil, err
}
return &check.CheckResp{
Found: true,
Price: resp.Price,
}, nil
// 手动代码结束
}
```
至此代码修改完成,凡事手动修改的代码我加了标注
## 11. 完整调用演示
* add api调用
```shell
curl -i "http://localhost:8888/add?book=go-zero&price=10"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 09:42:13 GMT
Content-Length: 11
{"ok":true}
```
* check api调用
```shell
curl -i "http://localhost:8888/check?book=go-zero"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 09:47:34 GMT
Content-Length: 25
{"found":true,"price":10}
```
## 12. Benchmark
因为写入依赖于mysql的写入速度就相当于压mysql了所以压测只测试了check接口相当于从mysql里读取并利用缓存为了方便直接压这一本书因为有缓存多本书也是一样的对压测结果没有影响。
压测之前,让我们先把打开文件句柄数调大:
```shel
ulimit -n 20000
```
并日志的等级改为`error`防止过多的info影响压测结果在每个yaml配置文件里加上如下
```yaml
Log:
Level: error
```
![Benchmark](images/bookstore-benchmark.png)
可以看出在我的MacBook Pro上能达到3万+的qps。
## 13. 完整代码
[https://github.com/tal-tech/go-zero/tree/master/example/bookstore](https://github.com/tal-tech/go-zero/tree/master/example/bookstore)
## 14. 总结
我们一直强调**工具大于约定和文档**。
go-zero不只是一个框架更是一个建立在框架+工具基础上的,简化和规范了整个微服务构建的技术体系。
我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。
通过go-zero+goctl生成的代码包含了微服务治理的各种组件包括并发控制、自适应熔断、自适应降载、自动缓存控制等可以轻松部署以承载巨大访问量。
有任何好的提升工程效率的想法,随时欢迎交流!👏

Some files were not shown because too many files have changed in this diff Show More