Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43e712d86a | ||
|
|
4db20677f7 | ||
|
|
6887fb22de | ||
|
|
50fbdbcfd7 | ||
|
|
c77b8489d7 | ||
|
|
eca4ed2cc0 | ||
|
|
744c18b7cb | ||
|
|
8d6f6f933e | ||
|
|
37c3b9f5c1 | ||
|
|
1f1dcd16e6 | ||
|
|
3285436f75 | ||
|
|
7f49bd8a31 | ||
|
|
9cd2015661 | ||
|
|
cf3a1020b0 | ||
|
|
ee19fb736b | ||
|
|
b0ccfb8eb4 | ||
|
|
444e5a711f | ||
|
|
8774d72ddb | ||
|
|
e3fcdbf040 | ||
|
|
2854ca03b4 | ||
|
|
6c624a6ed0 | ||
|
|
57b73d8b49 | ||
|
|
a79cee12ee | ||
|
|
7a921f66e6 | ||
|
|
12e235efb0 | ||
|
|
01060cf16d | ||
|
|
0786862a35 | ||
|
|
efa43483b2 | ||
|
|
771371e051 | ||
|
|
2ee95f8981 | ||
|
|
5bc01e4bfd | ||
|
|
510e966982 | ||
|
|
10e3b8ac80 | ||
|
|
04059bbf5a | ||
|
|
d643007c79 | ||
|
|
fc43876cc5 | ||
|
|
a926cb514f | ||
|
|
25cab2f273 | ||
|
|
8d2e2753a2 | ||
|
|
cc4c50e3eb | ||
|
|
751072bdb0 | ||
|
|
e97e1f10db | ||
|
|
0bd2a0656c | ||
|
|
71a2b20301 | ||
|
|
8df7de94e3 | ||
|
|
bf21203297 | ||
|
|
ae98375194 | ||
|
|
82d1ccf376 | ||
|
|
bb6d49c17e | ||
|
|
ed735ec47c | ||
|
|
ba4bac3a03 | ||
|
|
08433d7e04 | ||
|
|
a3b525b50d | ||
|
|
097f6886f2 | ||
|
|
07a1549634 | ||
|
|
befca26c58 | ||
|
|
3556a2eef4 | ||
|
|
807765f77e | ||
|
|
e44584e549 | ||
|
|
acd48f0abb | ||
|
|
f919bc6713 | ||
|
|
a0030b8f45 | ||
|
|
a5f0cce1b1 | ||
|
|
4d13dda605 | ||
|
|
b56cc8e459 | ||
|
|
c435811479 | ||
|
|
c686c93fb5 | ||
|
|
da8f76e6bd | ||
|
|
99596a4149 | ||
|
|
ec2a9f2c57 | ||
|
|
fd73ced6dc | ||
|
|
5071736ab4 | ||
|
|
0d7f1d23b4 | ||
|
|
84ab11ac09 | ||
|
|
67804a6bb2 | ||
|
|
65ee877236 | ||
|
|
b060867009 | ||
|
|
4d53045c6b | ||
|
|
cecd4b1b75 | ||
|
|
7cd0463953 | ||
|
|
7a82cf80ce | ||
|
|
f997aee3ba | ||
|
|
88ec89bdbd | ||
|
|
7d1b43780a | ||
|
|
4b5c2de376 | ||
|
|
e5c560e8ba | ||
|
|
bed494d904 | ||
|
|
2dfecda465 | ||
|
|
3ebb1e0221 | ||
|
|
348184904c | ||
|
|
7a27fa50a1 | ||
|
|
8d4951c990 | ||
|
|
6e57f6c527 | ||
|
|
b9ac51b6c3 | ||
|
|
702e8d79ce | ||
|
|
95a9dabf8b | ||
|
|
bae66c49c2 | ||
|
|
e0afe0b4bb | ||
|
|
24fb29a356 | ||
|
|
71083b5e64 | ||
|
|
1174f17bd9 | ||
|
|
d6d8fc21d8 | ||
|
|
9592639cb4 | ||
|
|
abcb28e506 | ||
|
|
a92f65580c | ||
|
|
3819f67cf4 | ||
|
|
295c8d2934 | ||
|
|
88da8685dd | ||
|
|
c7831ac96d | ||
|
|
e898761762 | ||
|
|
13d1c5cd00 | ||
|
|
16bfb1b7be | ||
|
|
ef4d4968d6 | ||
|
|
7b4a5e3ec6 | ||
|
|
e6df21e0d2 | ||
|
|
0a2c2d1eca | ||
|
|
a5fb29a6f0 | ||
|
|
f8da301e57 | ||
|
|
cb9075b737 | ||
|
|
3f389a55c2 | ||
|
|
afbd565d87 | ||
|
|
d629acc2b7 | ||
|
|
f32c6a9b28 | ||
|
|
95aa65efb9 | ||
|
|
3806e66cf1 | ||
|
|
bd430baf52 | ||
|
|
48f4154ea8 | ||
|
|
2599e0d28d | ||
|
|
12327fa07d | ||
|
|
57079bf4a4 | ||
|
|
7f6eceb5a3 | ||
|
|
7d7cb836af | ||
|
|
f87d9d1dda | ||
|
|
856b5aadb1 | ||
|
|
f7d778e0ed | ||
|
|
88333ee77f | ||
|
|
e76f44a35b | ||
|
|
c9ec22d5f4 | ||
|
|
afffc1048b | ||
|
|
d0b76b1d9a | ||
|
|
b004b070d7 | ||
|
|
677d581bd1 | ||
|
|
b776468e69 | ||
|
|
4c9315e984 | ||
|
|
668a7011c4 | ||
|
|
cc07a1d69b | ||
|
|
7f99a3baa8 | ||
|
|
9504418462 | ||
|
|
b144a2335c | ||
|
|
7b9ed7a313 | ||
|
|
3d2e9fcb84 | ||
|
|
2b993424c1 | ||
|
|
5e87b33b23 | ||
|
|
9b7cc43dcb | ||
|
|
000b28cf84 | ||
|
|
9fd16cd278 | ||
|
|
b71429e16b | ||
|
|
a13b48c33e | ||
|
|
033525fea8 | ||
|
|
607fc3297a | ||
|
|
4287877b74 | ||
|
|
2b7545ce11 | ||
|
|
60925c1164 | ||
|
|
1c9e81aa28 | ||
|
|
db7dcaa120 | ||
|
|
099d44054d | ||
|
|
f5f873c6bd | ||
|
|
6dbd3eada9 | ||
|
|
cf2d20a211 | ||
|
|
91bfc093f4 | ||
|
|
cf33aae91d | ||
|
|
c9494c8bc7 | ||
|
|
1fd2ef9347 | ||
|
|
efffb40fa3 | ||
|
|
9c8f31cf83 | ||
|
|
96cb7af728 | ||
|
|
41964f9d52 | ||
|
|
fe0d0687f5 | ||
|
|
1c1e4bca86 | ||
|
|
1abe21aa2a | ||
|
|
cee170f3e9 | ||
|
|
907efd92c9 | ||
|
|
737cd4751a | ||
|
|
dfe6e88529 | ||
|
|
85a815bea0 | ||
|
|
aa3c391919 | ||
|
|
c9b0ac1ee4 | ||
|
|
33faab61a3 | ||
|
|
81bf122fa4 | ||
|
|
a14bd309a9 | ||
|
|
ea7e410145 | ||
|
|
e81358e7fa | ||
|
|
695ea69bfc | ||
|
|
d2ed14002c | ||
|
|
1d9c4a4c4b | ||
|
|
7e83895c6e | ||
|
|
dc0534573c | ||
|
|
fe3739b7f3 | ||
|
|
94645481b1 | ||
|
|
338caf9927 | ||
|
|
9cc979960f | ||
|
|
f904710811 | ||
|
|
8291eabc2c | ||
|
|
901fadb5d3 | ||
|
|
c824e9e118 | ||
|
|
6f49639f80 | ||
|
|
7d4a548d29 | ||
|
|
936dd67008 | ||
|
|
84cc41df42 | ||
|
|
da1a93e932 | ||
|
|
7e61555d42 | ||
|
|
7a134ec64d | ||
|
|
d123b00e73 | ||
|
|
20d53add46 | ||
|
|
a1b141d31a | ||
|
|
0a9c427443 | ||
|
|
c32759d735 |
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '18 19 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
2
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
# Unignore all with extensions
|
||||
!*.*
|
||||
!**/Dockerfile
|
||||
!**/Makefile
|
||||
|
||||
# Unignore all dirs
|
||||
!*/
|
||||
@@ -12,7 +13,6 @@
|
||||
.idea
|
||||
**/.DS_Store
|
||||
**/logs
|
||||
!Makefile
|
||||
|
||||
# gitlab ci
|
||||
.cache
|
||||
|
||||
@@ -37,7 +37,6 @@ type (
|
||||
|
||||
BloomFilter struct {
|
||||
bits uint
|
||||
maps uint
|
||||
bitSet BitSetProvider
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,19 +3,15 @@ package bloom
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis/redistest"
|
||||
)
|
||||
|
||||
func TestRedisBitSet_New_Set_Test(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error("Miniredis could not start")
|
||||
}
|
||||
defer s.Close()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
store := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
bitSet := newRedisBitSet(store, "test_key", 1024)
|
||||
isSetBefore, err := bitSet.check([]uint{0})
|
||||
if err != nil {
|
||||
@@ -46,13 +42,10 @@ func TestRedisBitSet_New_Set_Test(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedisBitSet_Add(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error("Miniredis could not start")
|
||||
}
|
||||
defer s.Close()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
store := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
filter := New(store, "test_key", 64)
|
||||
assert.Nil(t, filter.Add([]byte("hello")))
|
||||
assert.Nil(t, filter.Add([]byte("world")))
|
||||
|
||||
@@ -25,7 +25,7 @@ type (
|
||||
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.
|
||||
@@ -34,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()
|
||||
@@ -89,7 +89,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func NewBreaker(opts ...BreakerOption) Breaker {
|
||||
func NewBreaker(opts ...Option) Breaker {
|
||||
var b circuitBreaker
|
||||
for _, opt := range opts {
|
||||
opt(&b)
|
||||
@@ -127,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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package breaker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -157,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())
|
||||
|
||||
@@ -146,15 +146,15 @@ func EcbEncryptBase64(key, src string) (string, error) {
|
||||
}
|
||||
|
||||
func getKeyBytes(key string) ([]byte, error) {
|
||||
if len(key) > 32 {
|
||||
if keyBytes, err := base64.StdEncoding.DecodeString(key); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return keyBytes, nil
|
||||
}
|
||||
if len(key) <= 32 {
|
||||
return []byte(key), nil
|
||||
}
|
||||
|
||||
return []byte(key), nil
|
||||
if keyBytes, err := base64.StdEncoding.DecodeString(key); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return keyBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
|
||||
64
core/codec/aesecb_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAesEcb(t *testing.T) {
|
||||
var (
|
||||
key = []byte("q4t7w!z%C*F-JaNdRgUjXn2r5u8x/A?D")
|
||||
val = []byte("hello")
|
||||
badKey1 = []byte("aaaaaaaaa")
|
||||
// more than 32 chars
|
||||
badKey2 = []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
)
|
||||
_, err := EcbEncrypt(badKey1, val)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbEncrypt(badKey2, val)
|
||||
assert.NotNil(t, err)
|
||||
dst, err := EcbEncrypt(key, val)
|
||||
assert.Nil(t, err)
|
||||
_, err = EcbDecrypt(badKey1, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecrypt(badKey2, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecrypt(key, val)
|
||||
// not enough block, just nil
|
||||
assert.Nil(t, err)
|
||||
src, err := EcbDecrypt(key, dst)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, val, src)
|
||||
}
|
||||
|
||||
func TestAesEcbBase64(t *testing.T) {
|
||||
const (
|
||||
val = "hello"
|
||||
badKey1 = "aaaaaaaaa"
|
||||
// more than 32 chars
|
||||
badKey2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
var key = []byte("q4t7w!z%C*F-JaNdRgUjXn2r5u8x/A?D")
|
||||
b64Key := base64.StdEncoding.EncodeToString(key)
|
||||
b64Val := base64.StdEncoding.EncodeToString([]byte(val))
|
||||
_, err := EcbEncryptBase64(badKey1, val)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbEncryptBase64(badKey2, val)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbEncryptBase64(b64Key, val)
|
||||
assert.NotNil(t, err)
|
||||
dst, err := EcbEncryptBase64(b64Key, b64Val)
|
||||
assert.Nil(t, err)
|
||||
_, err = EcbDecryptBase64(badKey1, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecryptBase64(badKey2, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecryptBase64(b64Key, val)
|
||||
assert.NotNil(t, err)
|
||||
src, err := EcbDecryptBase64(b64Key, dst)
|
||||
b, err := base64.StdEncoding.DecodeString(src)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, val, string(b))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ FstHGSkUYFLe+nl1dEKHbD+/Zt95L757J3xGTrwoTc7KCTxbrgn+stn0w52BNjj/
|
||||
kIE2ko4lbh/v8Fl14AyVR9msfKtKOnhe5FCT72mdtApr+qvzcC3q9hfXwkyQU32p
|
||||
v7q5UimZ205iKSBmgQIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
testBody = `this is the content`
|
||||
encryptedBody = `49e7bc15640e5d927fd3f129b749536d0755baf03a0f35fc914ff1b7b8ce659e5fe3a598442eb908c5995e28bacd3d76e4420bb05b6bfc177040f66c6976f680f7123505d626ab96a9db1151f45c93bc0262db9087b9fb6801715f76f902e644a20029262858f05b0d10540842204346ac1d6d8f29cc5d47dab79af75d922ef2`
|
||||
testBody = `this is the content`
|
||||
)
|
||||
|
||||
func TestCryption(t *testing.T) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// RollingWindowOption let callers customize the RollingWindow.
|
||||
RollingWindowOption func(rollingWindow *RollingWindow)
|
||||
|
||||
// RollingWindow defines a rolling window to calculate the events in buckets with time interval.
|
||||
RollingWindow struct {
|
||||
lock sync.RWMutex
|
||||
size int
|
||||
@@ -17,11 +19,17 @@ type (
|
||||
interval time.Duration
|
||||
offset int
|
||||
ignoreCurrent bool
|
||||
lastTime time.Duration
|
||||
lastTime time.Duration // start time of the last bucket
|
||||
}
|
||||
)
|
||||
|
||||
// NewRollingWindow returns a RollingWindow that with size buckets and time interval,
|
||||
// use opts to customize the RollingWindow.
|
||||
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),
|
||||
@@ -34,6 +42,7 @@ func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOpt
|
||||
return w
|
||||
}
|
||||
|
||||
// Add adds value to current bucket.
|
||||
func (rw *RollingWindow) Add(v float64) {
|
||||
rw.lock.Lock()
|
||||
defer rw.lock.Unlock()
|
||||
@@ -41,6 +50,7 @@ func (rw *RollingWindow) Add(v float64) {
|
||||
rw.win.add(rw.offset, v)
|
||||
}
|
||||
|
||||
// Reduce runs fn on all buckets, ignore current bucket if ignoreCurrent was set.
|
||||
func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
|
||||
rw.lock.RLock()
|
||||
defer rw.lock.RUnlock()
|
||||
@@ -70,29 +80,23 @@ func (rw *RollingWindow) span() int {
|
||||
|
||||
func (rw *RollingWindow) updateOffset() {
|
||||
span := rw.span()
|
||||
if span > 0 {
|
||||
offset := rw.offset
|
||||
// reset expired buckets
|
||||
start := offset + 1
|
||||
steps := start + span
|
||||
var remainder int
|
||||
if steps > rw.size {
|
||||
remainder = steps - rw.size
|
||||
steps = rw.size
|
||||
}
|
||||
for i := start; i < steps; i++ {
|
||||
rw.win.resetBucket(i)
|
||||
offset = i
|
||||
}
|
||||
for i := 0; i < remainder; i++ {
|
||||
rw.win.resetBucket(i)
|
||||
offset = i
|
||||
}
|
||||
rw.offset = offset
|
||||
rw.lastTime = timex.Now()
|
||||
if span <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
offset := rw.offset
|
||||
// reset expired buckets
|
||||
for i := 0; i < span; i++ {
|
||||
rw.win.resetBucket((offset + i + 1) % rw.size)
|
||||
}
|
||||
|
||||
rw.offset = (offset + span) % rw.size
|
||||
now := timex.Now()
|
||||
// align to interval time boundary
|
||||
rw.lastTime = now - (now-rw.lastTime)%rw.interval
|
||||
}
|
||||
|
||||
// Bucket defines the bucket that holds sum and num of additions.
|
||||
type Bucket struct {
|
||||
Sum float64
|
||||
Count int64
|
||||
@@ -114,9 +118,9 @@ type window struct {
|
||||
}
|
||||
|
||||
func newWindow(size int) *window {
|
||||
var buckets []*Bucket
|
||||
buckets := make([]*Bucket, size)
|
||||
for i := 0; i < size; i++ {
|
||||
buckets = append(buckets, new(Bucket))
|
||||
buckets[i] = new(Bucket)
|
||||
}
|
||||
return &window{
|
||||
buckets: buckets,
|
||||
@@ -130,14 +134,15 @@ func (w *window) add(offset int, v float64) {
|
||||
|
||||
func (w *window) reduce(start, count int, fn func(b *Bucket)) {
|
||||
for i := 0; i < count; i++ {
|
||||
fn(w.buckets[(start+i)%len(w.buckets)])
|
||||
fn(w.buckets[(start+i)%w.size])
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) resetBucket(offset int) {
|
||||
w.buckets[offset].reset()
|
||||
w.buckets[offset%w.size].reset()
|
||||
}
|
||||
|
||||
// IgnoreCurrentBucket lets the Reduce call ignore current bucket.
|
||||
func IgnoreCurrentBucket() RollingWindowOption {
|
||||
return func(w *RollingWindow) {
|
||||
w.ignoreCurrent = true
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -98,6 +105,37 @@ func TestRollingWindowReduce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollingWindowBucketTimeBoundary(t *testing.T) {
|
||||
const size = 3
|
||||
interval := time.Millisecond * 30
|
||||
r := NewRollingWindow(size, interval)
|
||||
listBuckets := func() []float64 {
|
||||
var buckets []float64
|
||||
r.Reduce(func(b *Bucket) {
|
||||
buckets = append(buckets, b.Sum)
|
||||
})
|
||||
return buckets
|
||||
}
|
||||
assert.Equal(t, []float64{0, 0, 0}, listBuckets())
|
||||
r.Add(1)
|
||||
assert.Equal(t, []float64{0, 0, 1}, listBuckets())
|
||||
time.Sleep(time.Millisecond * 45)
|
||||
r.Add(2)
|
||||
r.Add(3)
|
||||
assert.Equal(t, []float64{0, 1, 5}, listBuckets())
|
||||
// sleep time should be less than interval, and make the bucket change happen
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
r.Add(4)
|
||||
r.Add(5)
|
||||
r.Add(6)
|
||||
assert.Equal(t, []float64{1, 5, 15}, listBuckets())
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
r.Add(7)
|
||||
r.Add(8)
|
||||
r.Add(9)
|
||||
assert.Equal(t, []float64{0, 0, 24}, listBuckets())
|
||||
}
|
||||
|
||||
func TestRollingWindowDataRace(t *testing.T) {
|
||||
const size = 3
|
||||
r := NewRollingWindow(size, duration)
|
||||
|
||||
@@ -204,6 +204,7 @@ func (tw *TimingWheel) removeTask(key interface{}) {
|
||||
|
||||
timer := val.(*positionEntry)
|
||||
timer.item.removed = true
|
||||
tw.timers.Del(key)
|
||||
}
|
||||
|
||||
func (tw *TimingWheel) run() {
|
||||
@@ -248,7 +249,6 @@ func (tw *TimingWheel) scanAndRunTasks(l *list.List) {
|
||||
if task.removed {
|
||||
next := e.Next()
|
||||
l.Remove(e)
|
||||
tw.timers.Del(task.key)
|
||||
e = next
|
||||
continue
|
||||
} else if task.circle > 0 {
|
||||
@@ -301,6 +301,7 @@ func (tw *TimingWheel) setTask(task *timingEntry) {
|
||||
func (tw *TimingWheel) setTimerPosition(pos int, task *timingEntry) {
|
||||
if val, ok := tw.timers.Get(task.key); ok {
|
||||
timer := val.(*positionEntry)
|
||||
timer.item = task
|
||||
timer.pos = pos
|
||||
} else {
|
||||
tw.timers.Set(task.key, &positionEntry{
|
||||
|
||||
@@ -594,6 +594,31 @@ func TestTimingWheel_ElapsedAndSetThenMove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveAndRemoveTask(t *testing.T) {
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func(v int) {
|
||||
for i := 0; i < v; i++ {
|
||||
ticker.Tick()
|
||||
}
|
||||
}
|
||||
var keys []int
|
||||
tw, _ := newTimingWheelWithClock(testStep, 10, func(k, v interface{}) {
|
||||
assert.Equal(t, "any", k)
|
||||
assert.Equal(t, 3, v.(int))
|
||||
keys = append(keys, v.(int))
|
||||
ticker.Done()
|
||||
}, ticker)
|
||||
defer tw.Stop()
|
||||
tw.SetTimer("any", 3, testStep*8)
|
||||
tick(6)
|
||||
tw.MoveTimer("any", testStep*7)
|
||||
tick(3)
|
||||
tw.RemoveTimer("any")
|
||||
tick(30)
|
||||
time.Sleep(time.Millisecond)
|
||||
assert.Equal(t, 0, len(keys))
|
||||
}
|
||||
|
||||
func BenchmarkTimingWheel(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/mapping"
|
||||
@@ -19,7 +20,7 @@ func LoadConfig(file string, v interface{}) error {
|
||||
if content, err := ioutil.ReadFile(file); err != nil {
|
||||
return err
|
||||
} else if loader, ok := loaders[path.Ext(file)]; ok {
|
||||
return loader(content, v)
|
||||
return loader([]byte(os.ExpandEnv(string(content))), v)
|
||||
} else {
|
||||
return fmt.Errorf("unrecoginized file type: %s", file)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,21 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/fs"
|
||||
"github.com/tal-tech/go-zero/core/hash"
|
||||
)
|
||||
|
||||
func TestLoadConfig_notExists(t *testing.T) {
|
||||
assert.NotNil(t, LoadConfig("not_a_file", nil))
|
||||
}
|
||||
|
||||
func TestLoadConfig_notRecogFile(t *testing.T) {
|
||||
filename, err := fs.TempFilenameWithText("hello")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(filename)
|
||||
assert.NotNil(t, LoadConfig(filename, nil))
|
||||
}
|
||||
|
||||
func TestConfigJson(t *testing.T) {
|
||||
tests := []string{
|
||||
".json",
|
||||
@@ -17,13 +29,14 @@ func TestConfigJson(t *testing.T) {
|
||||
}
|
||||
text := `{
|
||||
"a": "foo",
|
||||
"b": 1
|
||||
"b": 1,
|
||||
"c": "${FOO}"
|
||||
}`
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
os.Setenv("FOO", "2")
|
||||
defer os.Unsetenv("FOO")
|
||||
tmpfile, err := createTempFile(test, text)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpfile)
|
||||
@@ -31,10 +44,12 @@ func TestConfigJson(t *testing.T) {
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "2", val.C)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@ func TestProperties(t *testing.T) {
|
||||
assert.Equal(t, "test", props.GetString("app.name"))
|
||||
assert.Equal(t, "app", props.GetString("app.program"))
|
||||
assert.Equal(t, 5, props.GetInt("app.threads"))
|
||||
|
||||
val := props.ToString()
|
||||
assert.Contains(t, val, "app.name")
|
||||
assert.Contains(t, val, "app.program")
|
||||
assert.Contains(t, val, "app.threads")
|
||||
}
|
||||
|
||||
func TestLoadProperties_badContent(t *testing.T) {
|
||||
filename, err := fs.TempFilenameWithText("hello")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(filename)
|
||||
_, err = LoadProperties(filename)
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, len(err.Error()) > 0)
|
||||
}
|
||||
|
||||
func TestSetString(t *testing.T) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
indexOfKey = iota
|
||||
_ = iota
|
||||
indexOfId
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ type (
|
||||
disconnected bool
|
||||
currentState connectivity.State
|
||||
listeners []func()
|
||||
lock sync.Mutex
|
||||
// lock only guards listeners, because only listens can be accessed by other goroutines.
|
||||
lock sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
@@ -32,27 +33,33 @@ func (sw *stateWatcher) addListener(l func()) {
|
||||
sw.lock.Unlock()
|
||||
}
|
||||
|
||||
func (sw *stateWatcher) notifyListeners() {
|
||||
sw.lock.Lock()
|
||||
defer sw.lock.Unlock()
|
||||
|
||||
for _, l := range sw.listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *stateWatcher) updateState(conn etcdConn) {
|
||||
sw.currentState = conn.GetState()
|
||||
switch sw.currentState {
|
||||
case connectivity.TransientFailure, connectivity.Shutdown:
|
||||
sw.disconnected = true
|
||||
case connectivity.Ready:
|
||||
if sw.disconnected {
|
||||
sw.disconnected = false
|
||||
sw.notifyListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *stateWatcher) watch(conn etcdConn) {
|
||||
sw.currentState = conn.GetState()
|
||||
for {
|
||||
if conn.WaitForStateChange(context.Background(), sw.currentState) {
|
||||
newState := conn.GetState()
|
||||
sw.lock.Lock()
|
||||
sw.currentState = newState
|
||||
|
||||
switch newState {
|
||||
case connectivity.TransientFailure, connectivity.Shutdown:
|
||||
sw.disconnected = true
|
||||
case connectivity.Ready:
|
||||
if sw.disconnected {
|
||||
sw.disconnected = false
|
||||
for _, l := range sw.listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sw.lock.Unlock()
|
||||
sw.updateState(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd0:2379
|
||||
- http://etcd0.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -107,7 +107,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd1:2379
|
||||
- http://etcd1.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -179,7 +179,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd2:2379
|
||||
- http://etcd2.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -251,7 +251,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd3:2379
|
||||
- http://etcd3.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -323,7 +323,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd4:2379
|
||||
- http://etcd4.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
|
||||
11
core/errorx/callchain.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package errorx
|
||||
|
||||
func Chain(fns ...func() error) error {
|
||||
for _, fn := range fns {
|
||||
if err := fn(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
27
core/errorx/callchain_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
var errDummy = errors.New("dummy")
|
||||
assert.Nil(t, Chain(func() error {
|
||||
return nil
|
||||
}, func() error {
|
||||
return nil
|
||||
}))
|
||||
assert.Equal(t, errDummy, Chain(func() error {
|
||||
return errDummy
|
||||
}, func() error {
|
||||
return nil
|
||||
}))
|
||||
assert.Equal(t, errDummy, Chain(func() error {
|
||||
return nil
|
||||
}, func() error {
|
||||
return errDummy
|
||||
}))
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -3,6 +3,7 @@ package executors
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
@@ -35,6 +36,7 @@ type (
|
||||
// avoid race condition on waitGroup when calling wg.Add/Done/Wait(...)
|
||||
wgBarrier syncx.Barrier
|
||||
confirmChan chan lang.PlaceholderType
|
||||
inflight int32
|
||||
guarded bool
|
||||
newTicker func(duration time.Duration) timex.Ticker
|
||||
lock sync.Mutex
|
||||
@@ -49,7 +51,7 @@ func NewPeriodicalExecutor(interval time.Duration, container TaskContainer) *Per
|
||||
container: container,
|
||||
confirmChan: make(chan lang.PlaceholderType),
|
||||
newTicker: func(d time.Duration) timex.Ticker {
|
||||
return timex.NewTicker(interval)
|
||||
return timex.NewTicker(d)
|
||||
},
|
||||
}
|
||||
proc.AddShutdownListener(func() {
|
||||
@@ -82,6 +84,7 @@ func (pe *PeriodicalExecutor) Sync(fn func()) {
|
||||
}
|
||||
|
||||
func (pe *PeriodicalExecutor) Wait() {
|
||||
pe.Flush()
|
||||
pe.wgBarrier.Guard(func() {
|
||||
pe.waitGroup.Wait()
|
||||
})
|
||||
@@ -90,18 +93,16 @@ func (pe *PeriodicalExecutor) Wait() {
|
||||
func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool) {
|
||||
pe.lock.Lock()
|
||||
defer func() {
|
||||
var start bool
|
||||
if !pe.guarded {
|
||||
pe.guarded = true
|
||||
start = true
|
||||
// defer to unlock quickly
|
||||
defer pe.backgroundFlush()
|
||||
}
|
||||
pe.lock.Unlock()
|
||||
if start {
|
||||
pe.backgroundFlush()
|
||||
}
|
||||
}()
|
||||
|
||||
if pe.container.AddTask(task) {
|
||||
atomic.AddInt32(&pe.inflight, 1)
|
||||
return pe.container.RemoveAll(), true
|
||||
}
|
||||
|
||||
@@ -110,6 +111,9 @@ func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool)
|
||||
|
||||
func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
threading.GoSafe(func() {
|
||||
// flush before quit goroutine to avoid missing tasks
|
||||
defer pe.Flush()
|
||||
|
||||
ticker := pe.newTicker(pe.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -119,6 +123,7 @@ func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
select {
|
||||
case vals := <-pe.commander:
|
||||
commanded = true
|
||||
atomic.AddInt32(&pe.inflight, -1)
|
||||
pe.enterExecution()
|
||||
pe.confirmChan <- lang.Placeholder
|
||||
pe.executeTasks(vals)
|
||||
@@ -128,13 +133,7 @@ func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
commanded = false
|
||||
} else if pe.Flush() {
|
||||
last = timex.Now()
|
||||
} else if timex.Since(last) > pe.interval*idleRound {
|
||||
pe.lock.Lock()
|
||||
pe.guarded = false
|
||||
pe.lock.Unlock()
|
||||
|
||||
// flush again to avoid missing tasks
|
||||
pe.Flush()
|
||||
} else if pe.shallQuit(last) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -177,3 +176,19 @@ func (pe *PeriodicalExecutor) hasTasks(tasks interface{}) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *PeriodicalExecutor) shallQuit(last time.Duration) (stop bool) {
|
||||
if timex.Since(last) <= pe.interval*idleRound {
|
||||
return
|
||||
}
|
||||
|
||||
// checking pe.inflight and setting pe.guarded should be locked together
|
||||
pe.lock.Lock()
|
||||
if atomic.LoadInt32(&pe.inflight) == 0 {
|
||||
pe.guarded = false
|
||||
stop = true
|
||||
}
|
||||
pe.lock.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,6 +140,26 @@ func TestPeriodicalExecutor_WaitFast(t *testing.T) {
|
||||
assert.Equal(t, total, cnt)
|
||||
}
|
||||
|
||||
func TestPeriodicalExecutor_Deadlock(t *testing.T) {
|
||||
executor := NewBulkExecutor(func(tasks []interface{}) {
|
||||
}, WithBulkTasks(1), WithBulkInterval(time.Millisecond))
|
||||
for i := 0; i < 1e5; i++ {
|
||||
executor.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodicalExecutor_hasTasks(t *testing.T) {
|
||||
ticker := timex.NewFakeTicker()
|
||||
defer ticker.Stop()
|
||||
|
||||
exec := NewPeriodicalExecutor(time.Millisecond, newContainer(time.Millisecond, nil))
|
||||
exec.newTicker = func(d time.Duration) timex.Ticker {
|
||||
return ticker
|
||||
}
|
||||
assert.False(t, exec.hasTasks(nil))
|
||||
assert.True(t, exec.hasTasks(1))
|
||||
}
|
||||
|
||||
// go test -benchtime 10s -bench .
|
||||
func BenchmarkExecutor(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
@@ -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
|
||||
@@ -159,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() {
|
||||
@@ -243,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() {
|
||||
|
||||
@@ -169,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) {
|
||||
@@ -275,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) {
|
||||
@@ -286,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{}) {
|
||||
|
||||
@@ -3,9 +3,10 @@ package limit
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis/redistest"
|
||||
)
|
||||
|
||||
func TestPeriodLimit_Take(t *testing.T) {
|
||||
@@ -33,16 +34,16 @@ func TestPeriodLimit_RedisUnavailable(t *testing.T) {
|
||||
}
|
||||
|
||||
func testPeriodLimit(t *testing.T, opts ...LimitOption) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
const (
|
||||
seconds = 1
|
||||
total = 100
|
||||
quota = 5
|
||||
)
|
||||
l := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), "periodlimit", opts...)
|
||||
l := NewPeriodLimit(seconds, quota, store, "periodlimit", opts...)
|
||||
var allowed, hitQuota, overQuota int
|
||||
for i := 0; i < total; i++ {
|
||||
val, err := l.Take("first")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis/redistest"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -44,16 +45,16 @@ func TestTokenLimit_Rescue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTokenLimit_Take(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
const (
|
||||
total = 100
|
||||
rate = 5
|
||||
burst = 10
|
||||
)
|
||||
l := NewTokenLimiter(rate, burst, redis.NewRedis(s.Addr(), redis.NodeType), "tokenlimit")
|
||||
l := NewTokenLimiter(rate, burst, store, "tokenlimit")
|
||||
var allowed int
|
||||
for i := 0; i < total; i++ {
|
||||
time.Sleep(time.Second / time.Duration(total))
|
||||
@@ -66,16 +67,16 @@ func TestTokenLimit_Take(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTokenLimit_TakeBurst(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
const (
|
||||
total = 100
|
||||
rate = 5
|
||||
burst = 10
|
||||
)
|
||||
l := NewTokenLimiter(rate, burst, redis.NewRedis(s.Addr(), redis.NodeType), "tokenlimit")
|
||||
l := NewTokenLimiter(rate, burst, store, "tokenlimit")
|
||||
var allowed int
|
||||
for i := 0; i < total; i++ {
|
||||
if l.Allow() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -84,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
|
||||
@@ -229,8 +237,10 @@ func TestSetup(t *testing.T) {
|
||||
|
||||
func TestDisable(t *testing.T) {
|
||||
Disable()
|
||||
WithKeepDays(1)
|
||||
WithGzip()
|
||||
|
||||
var opt logOptions
|
||||
WithKeepDays(1)(&opt)
|
||||
WithGzip()(&opt)
|
||||
assert.Nil(t, Close())
|
||||
writeConsole = false
|
||||
assert.Nil(t, Close())
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ var mock tracespec.Trace = new(mockTrace)
|
||||
|
||||
func TestTraceLog(t *testing.T) {
|
||||
var buf mockWriter
|
||||
atomic.StoreUint32(&initialized, 1)
|
||||
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
|
||||
WithContext(ctx).(*traceLogger).write(&buf, levelInfo, testlog)
|
||||
assert.True(t, strings.Contains(buf.String(), mockTraceId))
|
||||
@@ -29,11 +30,11 @@ func TestTraceLog(t *testing.T) {
|
||||
|
||||
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)
|
||||
atomic.StoreUint32(&initialized, 1)
|
||||
errorLog = newLogWriter(log.New(&buf, "", flags))
|
||||
l.WithDuration(time.Second).Error(testlog)
|
||||
assert.True(t, strings.Contains(buf.String(), mockTraceId))
|
||||
assert.True(t, strings.Contains(buf.String(), mockSpanId))
|
||||
@@ -45,11 +46,11 @@ func TestTraceError(t *testing.T) {
|
||||
|
||||
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)
|
||||
atomic.StoreUint32(&initialized, 1)
|
||||
infoLog = newLogWriter(log.New(&buf, "", flags))
|
||||
l.WithDuration(time.Second).Info(testlog)
|
||||
assert.True(t, strings.Contains(buf.String(), mockTraceId))
|
||||
assert.True(t, strings.Contains(buf.String(), mockSpanId))
|
||||
@@ -61,11 +62,11 @@ func TestTraceInfo(t *testing.T) {
|
||||
|
||||
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)
|
||||
atomic.StoreUint32(&initialized, 1)
|
||||
slowLog = newLogWriter(log.New(&buf, "", flags))
|
||||
l.WithDuration(time.Second).Slow(testlog)
|
||||
assert.True(t, strings.Contains(buf.String(), mockTraceId))
|
||||
assert.True(t, strings.Contains(buf.String(), mockSpanId))
|
||||
@@ -77,10 +78,10 @@ func TestTraceSlow(t *testing.T) {
|
||||
|
||||
func TestTraceWithoutContext(t *testing.T) {
|
||||
var buf mockWriter
|
||||
l := WithContext(context.Background()).(*traceLogger)
|
||||
SetLevel(InfoLevel)
|
||||
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))
|
||||
|
||||
@@ -153,58 +153,57 @@ func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fie
|
||||
key := strings.TrimSpace(segments[0])
|
||||
options := segments[1:]
|
||||
|
||||
if len(options) > 0 {
|
||||
var fieldOpts fieldOptions
|
||||
|
||||
for _, segment := range options {
|
||||
option := strings.TrimSpace(segment)
|
||||
switch {
|
||||
case option == stringOption:
|
||||
fieldOpts.FromString = true
|
||||
case strings.HasPrefix(option, optionalOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
switch len(segs) {
|
||||
case 1:
|
||||
fieldOpts.Optional = true
|
||||
case 2:
|
||||
fieldOpts.Optional = true
|
||||
fieldOpts.OptionalDep = segs[1]
|
||||
default:
|
||||
return "", nil, fmt.Errorf("field %s has wrong optional", field.Name)
|
||||
}
|
||||
case option == optionalOption:
|
||||
fieldOpts.Optional = true
|
||||
case strings.HasPrefix(option, optionsOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
if len(segs) != 2 {
|
||||
return "", nil, fmt.Errorf("field %s has wrong options", field.Name)
|
||||
} else {
|
||||
fieldOpts.Options = strings.Split(segs[1], optionSeparator)
|
||||
}
|
||||
case strings.HasPrefix(option, defaultOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
if len(segs) != 2 {
|
||||
return "", nil, fmt.Errorf("field %s has wrong default option", field.Name)
|
||||
} else {
|
||||
fieldOpts.Default = strings.TrimSpace(segs[1])
|
||||
}
|
||||
case strings.HasPrefix(option, rangeOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
if len(segs) != 2 {
|
||||
return "", nil, fmt.Errorf("field %s has wrong range", field.Name)
|
||||
}
|
||||
if nr, err := parseNumberRange(segs[1]); err != nil {
|
||||
return "", nil, err
|
||||
} else {
|
||||
fieldOpts.Range = nr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return key, &fieldOpts, nil
|
||||
if len(options) == 0 {
|
||||
return key, nil, nil
|
||||
}
|
||||
|
||||
return key, nil, nil
|
||||
var fieldOpts fieldOptions
|
||||
for _, segment := range options {
|
||||
option := strings.TrimSpace(segment)
|
||||
switch {
|
||||
case option == stringOption:
|
||||
fieldOpts.FromString = true
|
||||
case strings.HasPrefix(option, optionalOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
switch len(segs) {
|
||||
case 1:
|
||||
fieldOpts.Optional = true
|
||||
case 2:
|
||||
fieldOpts.Optional = true
|
||||
fieldOpts.OptionalDep = segs[1]
|
||||
default:
|
||||
return "", nil, fmt.Errorf("field %s has wrong optional", field.Name)
|
||||
}
|
||||
case option == optionalOption:
|
||||
fieldOpts.Optional = true
|
||||
case strings.HasPrefix(option, optionsOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
if len(segs) != 2 {
|
||||
return "", nil, fmt.Errorf("field %s has wrong options", field.Name)
|
||||
} else {
|
||||
fieldOpts.Options = strings.Split(segs[1], optionSeparator)
|
||||
}
|
||||
case strings.HasPrefix(option, defaultOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
if len(segs) != 2 {
|
||||
return "", nil, fmt.Errorf("field %s has wrong default option", field.Name)
|
||||
} else {
|
||||
fieldOpts.Default = strings.TrimSpace(segs[1])
|
||||
}
|
||||
case strings.HasPrefix(option, rangeOption):
|
||||
segs := strings.Split(option, equalToken)
|
||||
if len(segs) != 2 {
|
||||
return "", nil, fmt.Errorf("field %s has wrong range", field.Name)
|
||||
}
|
||||
if nr, err := parseNumberRange(segs[1]); err != nil {
|
||||
return "", nil, err
|
||||
} else {
|
||||
fieldOpts.Range = nr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return key, &fieldOpts, nil
|
||||
}
|
||||
|
||||
func implicitValueRequiredStruct(tag string, tp reflect.Type) (bool, error) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ package proc
|
||||
|
||||
import "time"
|
||||
|
||||
// AddShutdownListener returns fn itself on windows, lets callers call fn on their own.
|
||||
func AddShutdownListener(fn func()) func() {
|
||||
return nil
|
||||
return fn
|
||||
}
|
||||
|
||||
// AddWrapUpListener returns fn itself on windows, lets callers call fn on their own.
|
||||
func AddWrapUpListener(fn func()) func() {
|
||||
return nil
|
||||
return fn
|
||||
}
|
||||
|
||||
func SetTimeoutToForceQuit(duration time.Duration) {
|
||||
|
||||
16
core/prof/profilecenter_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package prof
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReport(t *testing.T) {
|
||||
once.Do(func() {})
|
||||
assert.NotContains(t, generateReport(), "foo")
|
||||
report("foo", time.Second)
|
||||
assert.Contains(t, generateReport(), "foo")
|
||||
report("foo", time.Second)
|
||||
}
|
||||
23
core/prof/profiler_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package prof
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/utils"
|
||||
)
|
||||
|
||||
func TestProfiler(t *testing.T) {
|
||||
EnableProfiling()
|
||||
Start()
|
||||
Report("foo", ProfilePoint{
|
||||
ElapsedTimer: utils.NewElapsedTimer(),
|
||||
})
|
||||
}
|
||||
|
||||
func TestNullProfiler(t *testing.T) {
|
||||
p := newNullProfiler()
|
||||
p.Start()
|
||||
p.Report("foo", ProfilePoint{
|
||||
ElapsedTimer: utils.NewElapsedTimer(),
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -92,11 +92,11 @@ func currentCgroup() (*cgroup, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
cgroups[subsys] = path.Join(cgroupDir, subsys)
|
||||
if strings.Contains(subsys, ",") {
|
||||
for _, k := range strings.Split(subsys, ",") {
|
||||
cgroups[k] = path.Join(cgroupDir, k)
|
||||
}
|
||||
// https://man7.org/linux/man-pages/man7/cgroups.7.html
|
||||
// comma-separated list of controllers for cgroup version 1
|
||||
fields := strings.Split(subsys, ",")
|
||||
for _, val := range fields {
|
||||
cgroups[val] = path.Join(cgroupDir, val)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
core/stores/cache/cache_test.go
vendored
@@ -8,11 +8,11 @@ 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"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis/redistest"
|
||||
"github.com/tal-tech/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
@@ -76,23 +76,23 @@ 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 := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean1()
|
||||
r2, clean2, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean2()
|
||||
conf := ClusterConf{
|
||||
{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: r1.Addr(),
|
||||
Host: r1.Addr,
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: r2.Addr(),
|
||||
Host: r2.Addr,
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
@@ -124,13 +124,13 @@ func TestCache_SetDel(t *testing.T) {
|
||||
|
||||
func TestCache_OneNode(t *testing.T) {
|
||||
const total = 1000
|
||||
r := miniredis.NewMiniRedis()
|
||||
assert.Nil(t, r.Start())
|
||||
defer r.Close()
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
conf := ClusterConf{
|
||||
{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: r.Addr(),
|
||||
Host: r.Addr,
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
|
||||
8
core/stores/cache/cachenode.go
vendored
@@ -175,12 +175,12 @@ func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) e
|
||||
}
|
||||
if fresh {
|
||||
return nil
|
||||
} else {
|
||||
// got the result from previous ongoing query
|
||||
c.stat.IncrementTotal()
|
||||
c.stat.IncrementHit()
|
||||
}
|
||||
|
||||
// got the result from previous ongoing query
|
||||
c.stat.IncrementTotal()
|
||||
c.stat.IncrementHit()
|
||||
|
||||
return jsonx.Unmarshal(val.([]byte), v)
|
||||
}
|
||||
|
||||
|
||||
55
core/stores/cache/cachenode_test.go
vendored
@@ -9,12 +9,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"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/stores/redis/redistest"
|
||||
"github.com/tal-tech/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
@@ -26,12 +27,12 @@ func init() {
|
||||
}
|
||||
|
||||
func TestCacheNode_DelCache(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
cn := cacheNode{
|
||||
rds: redis.NewRedis(s.Addr(), redis.NodeType),
|
||||
rds: store,
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
lock: new(sync.Mutex),
|
||||
unstableExpiry: mathx.NewUnstable(expiryDeviation),
|
||||
@@ -70,12 +71,12 @@ func TestCacheNode_InvalidCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCacheNode_Take(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
cn := cacheNode{
|
||||
rds: redis.NewRedis(s.Addr(), redis.NodeType),
|
||||
rds: store,
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
barrier: syncx.NewSharedCalls(),
|
||||
lock: new(sync.Mutex),
|
||||
@@ -91,18 +92,18 @@ func TestCacheNode_Take(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "value", str)
|
||||
assert.Nil(t, cn.GetCache("any", &str))
|
||||
val, err := s.Get("any")
|
||||
val, err := store.Get("any")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, `"value"`, val)
|
||||
}
|
||||
|
||||
func TestCacheNode_TakeNotFound(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
cn := cacheNode{
|
||||
rds: redis.NewRedis(s.Addr(), redis.NodeType),
|
||||
rds: store,
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
barrier: syncx.NewSharedCalls(),
|
||||
lock: new(sync.Mutex),
|
||||
@@ -116,18 +117,18 @@ func TestCacheNode_TakeNotFound(t *testing.T) {
|
||||
})
|
||||
assert.Equal(t, errTestNotFound, err)
|
||||
assert.Equal(t, errTestNotFound, cn.GetCache("any", &str))
|
||||
val, err := s.Get("any")
|
||||
val, err := store.Get("any")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, `*`, val)
|
||||
|
||||
s.Set("any", "*")
|
||||
store.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")
|
||||
store.Del("any")
|
||||
var errDummy = errors.New("dummy")
|
||||
err = cn.Take(&str, "any", func(v interface{}) error {
|
||||
return errDummy
|
||||
@@ -136,12 +137,12 @@ func TestCacheNode_TakeNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCacheNode_TakeWithExpire(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
cn := cacheNode{
|
||||
rds: redis.NewRedis(s.Addr(), redis.NodeType),
|
||||
rds: store,
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
barrier: syncx.NewSharedCalls(),
|
||||
lock: new(sync.Mutex),
|
||||
@@ -157,18 +158,18 @@ func TestCacheNode_TakeWithExpire(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "value", str)
|
||||
assert.Nil(t, cn.GetCache("any", &str))
|
||||
val, err := s.Get("any")
|
||||
val, err := store.Get("any")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, `"value"`, val)
|
||||
}
|
||||
|
||||
func TestCacheNode_String(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer s.Close()
|
||||
defer clean()
|
||||
|
||||
cn := cacheNode{
|
||||
rds: redis.NewRedis(s.Addr(), redis.NodeType),
|
||||
rds: store,
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
barrier: syncx.NewSharedCalls(),
|
||||
lock: new(sync.Mutex),
|
||||
@@ -176,18 +177,16 @@ func TestCacheNode_String(t *testing.T) {
|
||||
stat: NewCacheStat("any"),
|
||||
errNotFound: errors.New("any"),
|
||||
}
|
||||
assert.Equal(t, s.Addr(), cn.String())
|
||||
assert.Equal(t, store.Addr, cn.String())
|
||||
}
|
||||
|
||||
func TestCacheValueWithBigInt(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer s.Close()
|
||||
store, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
cn := cacheNode{
|
||||
rds: redis.NewRedis(s.Addr(), redis.NodeType),
|
||||
rds: store,
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
barrier: syncx.NewSharedCalls(),
|
||||
lock: new(sync.Mutex),
|
||||
|
||||
25
core/stores/cache/cachestat.go
vendored
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ type (
|
||||
ZrevrangebyscoreWithScores(key string, start, stop int64) ([]redis.Pair, error)
|
||||
ZrevrangebyscoreWithScoresAndLimit(key string, start, stop int64, page, size int) ([]redis.Pair, error)
|
||||
Zscore(key string, value string) (int64, error)
|
||||
Zrevrank(key, field string) (int64, error)
|
||||
}
|
||||
|
||||
clusterStore struct {
|
||||
@@ -635,6 +636,15 @@ func (cs clusterStore) ZrevrangebyscoreWithScoresAndLimit(key string, start, sto
|
||||
return node.ZrevrangebyscoreWithScoresAndLimit(key, start, stop, page, size)
|
||||
}
|
||||
|
||||
func (cs clusterStore) Zrevrank(key, field string) (int64, error) {
|
||||
node, err := cs.getRedis(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return node.Zrevrank(key, field)
|
||||
}
|
||||
|
||||
func (cs clusterStore) Zscore(key string, value string) (int64, error) {
|
||||
node, err := cs.getRedis(key)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/hash"
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
@@ -516,6 +516,8 @@ func TestRedis_SortedSet(t *testing.T) {
|
||||
assert.NotNil(t, err)
|
||||
_, err = store.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
|
||||
assert.NotNil(t, err)
|
||||
_, err = store.Zrevrank("key", "value")
|
||||
assert.NotNil(t, err)
|
||||
_, err = store.Zadds("key", redis.Pair{
|
||||
Key: "value2",
|
||||
Score: 6,
|
||||
@@ -640,6 +642,9 @@ func TestRedis_SortedSet(t *testing.T) {
|
||||
Score: 5,
|
||||
},
|
||||
}, pairs)
|
||||
rank, err = client.Zrevrank("key", "value1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(1), rank)
|
||||
val, err = client.Zadds("key", redis.Pair{
|
||||
Key: "value2",
|
||||
Score: 6,
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
"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"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis/redistest"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -27,12 +27,10 @@ func init() {
|
||||
|
||||
func TestStat(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
|
||||
c := newCollection(dummyConn{}, cach)
|
||||
|
||||
@@ -73,12 +71,10 @@ func TestStatCacheFails(t *testing.T) {
|
||||
|
||||
func TestStatDbFails(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
|
||||
c := newCollection(dummyConn{}, cach)
|
||||
|
||||
@@ -97,12 +93,10 @@ func TestStatDbFails(t *testing.T) {
|
||||
|
||||
func TestStatFromMemory(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
cach := cache.NewCacheNode(r, sharedCalls, stats, mgo.ErrNotFound)
|
||||
c := newCollection(dummyConn{}, cach)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
const postgreDriverName = "postgres"
|
||||
const postgresDriverName = "postgres"
|
||||
|
||||
func NewPostgre(datasource string, opts ...sqlx.SqlOption) sqlx.SqlConn {
|
||||
return sqlx.NewSqlConn(postgreDriverName, datasource, opts...)
|
||||
func NewPostgres(datasource string, opts ...sqlx.SqlOption) sqlx.SqlConn {
|
||||
return sqlx.NewSqlConn(postgresDriverName, datasource, opts...)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ type (
|
||||
red.Cmdable
|
||||
}
|
||||
|
||||
// GeoLocation is used with GeoAdd to add geospatial location.
|
||||
GeoLocation = red.GeoLocation
|
||||
// GeoRadiusQuery is used with GeoRadius to query geospatial index.
|
||||
GeoRadiusQuery = red.GeoRadiusQuery
|
||||
GeoPos = red.GeoPos
|
||||
|
||||
Pipeliner = red.Pipeliner
|
||||
|
||||
// Z represents sorted set member.
|
||||
@@ -173,6 +179,107 @@ func (s *Redis) Expireat(key string, expireTime int64) error {
|
||||
}, acceptable)
|
||||
}
|
||||
|
||||
func (s *Redis) GeoAdd(key string, geoLocation ...*GeoLocation) (val int64, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := conn.GeoAdd(key, geoLocation...).Result(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val = v
|
||||
return nil
|
||||
}
|
||||
}, acceptable)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) GeoDist(key string, member1, member2, unit string) (val float64, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := conn.GeoDist(key, member1, member2, unit).Result(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val = v
|
||||
return nil
|
||||
}
|
||||
}, acceptable)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) GeoHash(key string, members ...string) (val []string, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := conn.GeoHash(key, members...).Result(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val = v
|
||||
return nil
|
||||
}
|
||||
}, acceptable)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) GeoRadius(key string, longitude, latitude float64, query *GeoRadiusQuery) (val []GeoLocation, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := conn.GeoRadius(key, longitude, latitude, query).Result(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val = v
|
||||
return nil
|
||||
}
|
||||
}, acceptable)
|
||||
return
|
||||
}
|
||||
func (s *Redis) GeoRadiusByMember(key, member string, query *GeoRadiusQuery) (val []GeoLocation, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := conn.GeoRadiusByMember(key, member, query).Result(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val = v
|
||||
return nil
|
||||
}
|
||||
}, acceptable)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) GeoPos(key string, members ...string) (val []*GeoPos, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := conn.GeoPos(key, members...).Result(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
val = v
|
||||
return nil
|
||||
}
|
||||
}, acceptable)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) Get(key string) (val string, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
@@ -1273,6 +1380,20 @@ func (s *Redis) ZrevrangebyscoreWithScoresAndLimit(key string, start, stop int64
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) Zrevrank(key string, field string) (val int64, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err = conn.ZRevRank(key, field).Result()
|
||||
return err
|
||||
}, acceptable)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Redis) String() string {
|
||||
return s.Addr
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
red "github.com/go-redis/redis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -556,7 +556,7 @@ func TestRedis_SortedSet(t *testing.T) {
|
||||
val, err = client.Zscore("key", "value1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(5), val)
|
||||
val, err = NewRedis(client.Addr, "").Zadds("key")
|
||||
_, err = NewRedis(client.Addr, "").Zadds("key")
|
||||
assert.NotNil(t, err)
|
||||
val, err = client.Zadds("key", Pair{
|
||||
Key: "value2",
|
||||
@@ -567,9 +567,9 @@ func TestRedis_SortedSet(t *testing.T) {
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(2), val)
|
||||
pairs, err := NewRedis(client.Addr, "").ZRevRangeWithScores("key", 1, 3)
|
||||
_, err = NewRedis(client.Addr, "").ZRevRangeWithScores("key", 1, 3)
|
||||
assert.NotNil(t, err)
|
||||
pairs, err = client.ZRevRangeWithScores("key", 1, 3)
|
||||
pairs, err := client.ZRevRangeWithScores("key", 1, 3)
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, []Pair{
|
||||
{
|
||||
@@ -584,6 +584,9 @@ func TestRedis_SortedSet(t *testing.T) {
|
||||
rank, err := client.Zrank("key", "value2")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(1), rank)
|
||||
rank, err = client.Zrevrank("key", "value1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(2), rank)
|
||||
_, err = NewRedis(client.Addr, "").Zrank("key", "value4")
|
||||
assert.NotNil(t, err)
|
||||
_, err = client.Zrank("key", "value4")
|
||||
@@ -710,6 +713,8 @@ func TestRedis_SortedSet(t *testing.T) {
|
||||
pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(pairs))
|
||||
_, err = NewRedis(client.Addr, "").Zrevrank("key", "value")
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -811,6 +816,38 @@ func TestRedisBlpopEx(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisGeo(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
client.Ping()
|
||||
var geoLocation = []*GeoLocation{{Longitude: 13.361389, Latitude: 38.115556, Name: "Palermo"}, {Longitude: 15.087269, Latitude: 37.502669, Name: "Catania"}}
|
||||
v, err := client.GeoAdd("sicily", geoLocation...)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(2), v)
|
||||
v2, err := client.GeoDist("sicily", "Palermo", "Catania", "m")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 166274, int(v2))
|
||||
// GeoHash not support
|
||||
v3, err := client.GeoPos("sicily", "Palermo", "Catania")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(v3[0].Longitude), int64(13))
|
||||
assert.Equal(t, int64(v3[0].Latitude), int64(38))
|
||||
assert.Equal(t, int64(v3[1].Longitude), int64(15))
|
||||
assert.Equal(t, int64(v3[1].Latitude), int64(37))
|
||||
v4, err := client.GeoRadius("sicily", 15, 37, &red.GeoRadiusQuery{WithDist: true, Unit: "km", Radius: 200})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(v4[0].Dist), int64(190))
|
||||
assert.Equal(t, int64(v4[1].Dist), int64(56))
|
||||
var geoLocation2 = []*GeoLocation{{Longitude: 13.583333, Latitude: 37.316667, Name: "Agrigento"}}
|
||||
v5, err := client.GeoAdd("sicily", geoLocation2...)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(1), v5)
|
||||
v6, err := client.GeoRadiusByMember("sicily", "Agrigento", &red.GeoRadiusQuery{Unit: "km", Radius: 100})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, v6[0].Name, "Agrigento")
|
||||
assert.Equal(t, v6[1].Name, "Palermo")
|
||||
})
|
||||
}
|
||||
|
||||
func runOnRedis(t *testing.T, fn func(client *Redis)) {
|
||||
s, err := miniredis.Run()
|
||||
assert.Nil(t, err)
|
||||
|
||||
28
core/stores/redis/redistest/redistest.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package redistest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
func CreateRedis() (r *redis.Redis, clean func(), err error) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return redis.NewRedis(mr.Addr(), redis.NodeType), func() {
|
||||
ch := make(chan lang.PlaceholderType)
|
||||
go func() {
|
||||
mr.Close()
|
||||
close(ch)
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
@@ -14,12 +14,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/fx"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
"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/stores/redis/redistest"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
@@ -30,17 +32,15 @@ func init() {
|
||||
|
||||
func TestCachedConn_GetCache(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
||||
var value string
|
||||
err = c.GetCache("any", &value)
|
||||
assert.Equal(t, ErrNotFound, err)
|
||||
s.Set("any", `"value"`)
|
||||
r.Set("any", `"value"`)
|
||||
err = c.GetCache("any", &value)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "value", value)
|
||||
@@ -48,12 +48,10 @@ func TestCachedConn_GetCache(t *testing.T) {
|
||||
|
||||
func TestStat(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
@@ -73,16 +71,14 @@ func TestStat(t *testing.T) {
|
||||
|
||||
func TestCachedConn_QueryRowIndex_NoCache(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewConn(dummySqlConn{}, cache.CacheConf{
|
||||
{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: s.Addr(),
|
||||
Host: r.Addr,
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
@@ -124,12 +120,10 @@ func TestCachedConn_QueryRowIndex_NoCache(t *testing.T) {
|
||||
|
||||
func TestCachedConn_QueryRowIndex_HasCache(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
|
||||
cache.WithNotFoundExpiry(time.Second))
|
||||
|
||||
@@ -213,18 +207,13 @@ func TestCachedConn_QueryRowIndex_HasCache_IntPrimary(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
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, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
|
||||
cache.WithNotFoundExpiry(time.Second))
|
||||
|
||||
@@ -261,14 +250,10 @@ func TestCachedConn_QueryRowIndex_HasWrongCache(t *testing.T) {
|
||||
for k, v := range caches {
|
||||
t.Run(k+"/"+v, func(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
s.FlushAll()
|
||||
defer s.Close()
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10),
|
||||
cache.WithNotFoundExpiry(time.Second))
|
||||
|
||||
@@ -320,12 +305,10 @@ func TestStatCacheFails(t *testing.T) {
|
||||
|
||||
func TestStatDbFails(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
@@ -343,12 +326,10 @@ func TestStatDbFails(t *testing.T) {
|
||||
|
||||
func TestStatFromMemory(t *testing.T) {
|
||||
resetStats()
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
|
||||
|
||||
var all sync.WaitGroup
|
||||
@@ -403,10 +384,9 @@ func TestStatFromMemory(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCachedConnQueryRow(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
const (
|
||||
key = "user"
|
||||
@@ -415,7 +395,6 @@ func TestCachedConnQueryRow(t *testing.T) {
|
||||
var conn trackedConn
|
||||
var user string
|
||||
var ran bool
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
||||
err = c.QueryRow(&user, key, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
ran = true
|
||||
@@ -423,7 +402,7 @@ func TestCachedConnQueryRow(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
actualValue, err := s.Get(key)
|
||||
actualValue, err := r.Get(key)
|
||||
assert.Nil(t, err)
|
||||
var actual string
|
||||
assert.Nil(t, json.Unmarshal([]byte(actualValue), &actual))
|
||||
@@ -433,10 +412,9 @@ func TestCachedConnQueryRow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCachedConnQueryRowFromCache(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
const (
|
||||
key = "user"
|
||||
@@ -445,7 +423,6 @@ func TestCachedConnQueryRowFromCache(t *testing.T) {
|
||||
var conn trackedConn
|
||||
var user string
|
||||
var ran bool
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
||||
assert.Nil(t, c.SetCache(key, value))
|
||||
err = c.QueryRow(&user, key, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
@@ -454,7 +431,7 @@ func TestCachedConnQueryRowFromCache(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
actualValue, err := s.Get(key)
|
||||
actualValue, err := r.Get(key)
|
||||
assert.Nil(t, err)
|
||||
var actual string
|
||||
assert.Nil(t, json.Unmarshal([]byte(actualValue), &actual))
|
||||
@@ -464,16 +441,14 @@ func TestCachedConnQueryRowFromCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQueryRowNotFound(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
const key = "user"
|
||||
var conn trackedConn
|
||||
var user string
|
||||
var ran int
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
||||
for i := 0; i < 20; i++ {
|
||||
err = c.QueryRow(&user, key, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
@@ -486,13 +461,11 @@ func TestQueryRowNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCachedConnExec(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
var conn trackedConn
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
||||
_, err = c.ExecNoCache("delete from user_table where id='kevin'")
|
||||
assert.Nil(t, err)
|
||||
@@ -500,25 +473,26 @@ func TestCachedConnExec(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCachedConnExecDropCache(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, err := miniredis.Run()
|
||||
assert.Nil(t, err)
|
||||
defer fx.DoWithTimeout(func() error {
|
||||
r.Close()
|
||||
return nil
|
||||
}, time.Second)
|
||||
|
||||
const (
|
||||
key = "user"
|
||||
value = "any"
|
||||
)
|
||||
var conn trackedConn
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
|
||||
c := NewNodeConn(&conn, redis.NewRedis(r.Addr(), redis.NodeType), cache.WithExpiry(time.Second*30))
|
||||
assert.Nil(t, c.SetCache(key, value))
|
||||
_, err = c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
|
||||
return conn.Exec("delete from user_table where id='kevin'")
|
||||
}, key)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, conn.execValue)
|
||||
_, err = s.Get(key)
|
||||
_, err = r.Get(key)
|
||||
assert.Exactly(t, miniredis.ErrKeyNotFound, err)
|
||||
_, err = c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -539,13 +513,11 @@ func TestCachedConnExecDropCacheFailed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCachedConnQueryRows(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
var conn trackedConn
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
||||
var users []string
|
||||
err = c.QueryRowsNoCache(&users, "select user from user_table where id='kevin'")
|
||||
@@ -554,13 +526,11 @@ func TestCachedConnQueryRows(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCachedConnTransact(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
var conn trackedConn
|
||||
r := redis.NewRedis(s.Addr(), redis.NodeType)
|
||||
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
|
||||
err = c.Transact(func(session sqlx.Session) error {
|
||||
return nil
|
||||
@@ -570,10 +540,9 @@ func TestCachedConnTransact(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQueryRowNoCache(t *testing.T) {
|
||||
s, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
assert.Nil(t, err)
|
||||
defer clean()
|
||||
|
||||
const (
|
||||
key = "user"
|
||||
@@ -581,7 +550,6 @@ func TestQueryRowNoCache(t *testing.T) {
|
||||
)
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func NewSharedCalls() SharedCalls {
|
||||
}
|
||||
|
||||
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
|
||||
c, done := g.createCall(key, fn)
|
||||
c, done := g.createCall(key)
|
||||
if done {
|
||||
return c.val, c.err
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{
|
||||
}
|
||||
|
||||
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
|
||||
c, done := g.createCall(key, fn)
|
||||
c, done := g.createCall(key)
|
||||
if done {
|
||||
return c.val, false, c.err
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val inte
|
||||
return c.val, true, c.err
|
||||
}
|
||||
|
||||
func (g *sharedGroup) createCall(key string, fn func() (interface{}, error)) (c *call, done bool) {
|
||||
func (g *sharedGroup) createCall(key string) (c *call, done bool) {
|
||||
g.lock.Lock()
|
||||
if c, ok := g.calls[key]; ok {
|
||||
g.lock.Unlock()
|
||||
@@ -70,8 +70,6 @@ func (g *sharedGroup) createCall(key string, fn func() (interface{}, error)) (c
|
||||
|
||||
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()
|
||||
g.lock.Lock()
|
||||
delete(g.calls, key)
|
||||
g.lock.Unlock()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,624 +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, it’s 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`.
|
||||
|
||||
Let’s 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, you’ll find that it’s 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, let’s 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` and `bookstore/api`
|
||||
|
||||
* 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 let’s 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, don’t need to wait for the implementation of server side
|
||||
|
||||
## 6. Write code for add rpc service
|
||||
|
||||
- under directory `bookstore` create dir `rpc`
|
||||
|
||||
* 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, we’ve 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
|
||||
```
|
||||
|
||||

|
||||
|
||||
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 it’s easy to deal with the busy sites.
|
||||
|
||||
If you have any ideas that can help us to improve the productivity, tell me any time! 👏
|
||||
624
doc/bookstore.md
@@ -1,624 +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" />
|
||||
|
||||
下面我们来一起完整走一遍快速构建微服务的流程,Let’s `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/api`
|
||||
|
||||
* 在`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服务
|
||||
|
||||
- 在 `bookstore` 下创建 `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
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以看出在我的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生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。
|
||||
|
||||
有任何好的提升工程效率的想法,随时欢迎交流!👏
|
||||
@@ -1,5 +0,0 @@
|
||||
# 熔断机制设计
|
||||
|
||||
## 设计目的
|
||||
|
||||
* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务
|
||||
@@ -1,111 +0,0 @@
|
||||
# 通过 collection.Cache 进行缓存
|
||||
|
||||
go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等,本系列文章将分别介绍go-zero框架中工具的使用及其实现原理
|
||||
|
||||
## 进程内缓存工具[collection.Cache](https://github.com/tal-tech/go-zero/tree/master/core/collection/cache.go)
|
||||
|
||||
在做服务器开发的时候,相信都会遇到使用缓存的情况,go-zero 提供的简单的缓存封装 **collection.Cache**,简单使用方式如下
|
||||
|
||||
```go
|
||||
// 初始化 cache,其中 WithLimit 可以指定最大缓存的数量
|
||||
c, err := collection.NewCache(time.Minute, collection.WithLimit(10000))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置缓存
|
||||
c.Set("key", user)
|
||||
|
||||
// 获取缓存,ok:是否存在
|
||||
v, ok := c.Get("key")
|
||||
|
||||
// 删除缓存
|
||||
c.Del("key")
|
||||
|
||||
// 获取缓存,如果 key 不存在的,则会调用 func 去生成缓存
|
||||
v, err := c.Take("key", func() (interface{}, error) {
|
||||
return user, nil
|
||||
})
|
||||
```
|
||||
|
||||
cache 实现的建的功能包括
|
||||
|
||||
* 缓存自动失效,可以指定过期时间
|
||||
* 缓存大小限制,可以指定缓存个数
|
||||
* 缓存增删改
|
||||
* 缓存命中率统计
|
||||
* 并发安全
|
||||
* 缓存击穿
|
||||
|
||||
实现原理:
|
||||
Cache 自动失效,是采用 TimingWheel(https://github.com/tal-tech/go-zero/blob/master/core/collection/timingwheel.go) 进行管理的
|
||||
|
||||
``` go
|
||||
timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
|
||||
key, ok := k.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cache.Del(key)
|
||||
})
|
||||
```
|
||||
|
||||
Cache 大小限制,是采用 LRU 淘汰策略,在新增缓存的时候会去检查是否已经超出过限制,具体代码在 keyLru 中实现
|
||||
|
||||
``` go
|
||||
func (klru *keyLru) add(key string) {
|
||||
if elem, ok := klru.elements[key]; ok {
|
||||
klru.evicts.MoveToFront(elem)
|
||||
return
|
||||
}
|
||||
|
||||
// Add new item
|
||||
elem := klru.evicts.PushFront(key)
|
||||
klru.elements[key] = elem
|
||||
|
||||
// Verify size not exceeded
|
||||
if klru.evicts.Len() > klru.limit {
|
||||
klru.removeOldest()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cache 的命中率统计,是在代码中实现 cacheStat,在缓存命中丢失的时候自动统计,并且会定时打印使用的命中率, qps 等状态.
|
||||
|
||||
打印的具体效果如下
|
||||
|
||||
```go
|
||||
cache(proc) - qpm: 2, hit_ratio: 50.0%, elements: 0, hit: 1, miss: 1
|
||||
```
|
||||
|
||||
缓存击穿包含是使用 syncx.SharedCalls(https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go) 进行实现的,就是将同时请求同一个 key 的请求, 关于 sharedcalls 后续会继续补充。 相关具体实现是在:
|
||||
|
||||
```go
|
||||
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
|
||||
val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
|
||||
v, e := fetch()
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
c.Set(key, v)
|
||||
return v, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fresh {
|
||||
c.stats.IncrementMiss()
|
||||
return val, nil
|
||||
} else {
|
||||
// got the result from previous ongoing query
|
||||
c.stats.IncrementHit()
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
```
|
||||
|
||||
本文主要介绍了go-zero框架中的 Cache 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
|
||||
@@ -1,272 +0,0 @@
|
||||
# Goctl Model
|
||||
|
||||
goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
|
||||
|
||||
## 快速开始
|
||||
|
||||
* 通过ddl生成
|
||||
|
||||
```shell script
|
||||
goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
|
||||
```
|
||||
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
|
||||
```Plain Text
|
||||
model
|
||||
│ ├── error.go
|
||||
│ └── usermodel.go
|
||||
```
|
||||
|
||||
* 通过datasource生成
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"
|
||||
```
|
||||
|
||||
* 生成代码示例
|
||||
|
||||
``` go
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlc"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builderx.FieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
|
||||
cacheUserMobilePrefix = "cache#User#mobile#"
|
||||
cacheUserIdPrefix = "cache#User#id#"
|
||||
cacheUserNamePrefix = "cache#User#name#"
|
||||
)
|
||||
|
||||
type (
|
||||
UserModel struct {
|
||||
sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"name"` // 用户名称
|
||||
Password string `db:"password"` // 用户密码
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
Gender string `db:"gender"` // 男|女|未公开
|
||||
Nickname string `db:"nickname"` // 用户昵称
|
||||
CreateTime time.Time `db:"create_time"`
|
||||
UpdateTime time.Time `db:"update_time"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserModel {
|
||||
return &UserModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Insert(data User) (sql.Result, error) {
|
||||
query := `insert into ` + m.table + `(` + userRowsExpectAutoSet + `) value (?, ?, ?, ?, ?)`
|
||||
return m.ExecNoCache(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOne(id int64) (*User, error) {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
var resp User
|
||||
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByName(name string) (*User, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userNameKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where name = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userMobileKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where mobile = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, mobile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Update(data User) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `update ` + m.table + ` set ` + userRowsWithPlaceHolder + ` where id = ?`
|
||||
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
|
||||
}, userIdKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *UserModel) Delete(id int64) error {
|
||||
data, err := m.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `delete from ` + m.table + ` where id = ?`
|
||||
return conn.Exec(query, id)
|
||||
}, userIdKey, userNameKey, userMobileKey)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### 用法
|
||||
|
||||
```Plain Text
|
||||
goctl model mysql -h
|
||||
```
|
||||
|
||||
```Plain Text
|
||||
NAME:
|
||||
goctl model mysql - generate mysql model"
|
||||
|
||||
USAGE:
|
||||
goctl model mysql command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
ddl generate mysql model from ddl"
|
||||
datasource generate model from datasource"
|
||||
|
||||
OPTIONS:
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
## 生成规则
|
||||
|
||||
* 默认规则
|
||||
|
||||
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
|
||||
* 带缓存模式
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model mysql -src={filename} -dir={dir} -cache=true
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=true
|
||||
```
|
||||
|
||||
目前仅支持redis缓存,如果选择带缓存模式,即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码,目前仅支持单索引字段(除全文索引外),对于联合索引我们默认认为不需要带缓存,且不属于通用型代码,因此没有放在代码生成行列,如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
|
||||
|
||||
* 不带缓存模式
|
||||
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model -src={filename} -dir={dir}
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
|
||||
```
|
||||
|
||||
or
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model -src={filename} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
生成代码仅基本的CURD结构。
|
||||
|
||||
## 缓存
|
||||
|
||||
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
|
||||
|
||||
* 缓存会缓存哪些信息?
|
||||
|
||||
对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
|
||||
|
||||
* 数据有更新(`update`)操作会清空缓存吗?
|
||||
|
||||
会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。
|
||||
|
||||
* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
|
||||
|
||||
理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。
|
||||
|
||||
* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
|
||||
|
||||
目前,我认为除了基本的CURD外,其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
|
||||
|
||||
## QA
|
||||
|
||||
* goctl model除了命令行模式,支持插件模式吗?
|
||||
|
||||
很快支持idea插件。
|
||||
238
doc/goctl-rpc.md
@@ -1,238 +0,0 @@
|
||||
# Rpc Generation
|
||||
|
||||
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。
|
||||
|
||||
## 特性
|
||||
|
||||
* 简单易用
|
||||
* 快速提升开发效率
|
||||
* 出错率低
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:快速生成greet服务
|
||||
|
||||
通过命令 `goctl rpc new ${servieName}`生成
|
||||
|
||||
如生成greet rpc服务:
|
||||
|
||||
```shell script
|
||||
goctl rpc new greet
|
||||
```
|
||||
|
||||
执行后代码结构如下:
|
||||
|
||||
```golang
|
||||
└── greet
|
||||
├── etc
|
||||
│ └── greet.yaml
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── greet
|
||||
│ ├── greet.go
|
||||
│ ├── greet_mock.go
|
||||
│ └── types.go
|
||||
├── greet.go
|
||||
├── greet.proto
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── logic
|
||||
│ │ └── pinglogic.go
|
||||
│ ├── server
|
||||
│ │ └── greetserver.go
|
||||
│ └── svc
|
||||
│ └── servicecontext.go
|
||||
└── pb
|
||||
└── greet.pb.go
|
||||
```
|
||||
|
||||
rpc一键生成常见问题解决见 <a href="#常见问题解决">常见问题解决</a>
|
||||
|
||||
### 方式二:通过指定proto生成rpc服务
|
||||
|
||||
* 生成proto模板
|
||||
|
||||
```shell script
|
||||
goctl rpc template -o=user.proto
|
||||
```
|
||||
|
||||
```golang
|
||||
syntax = "proto3";
|
||||
|
||||
package remote;
|
||||
|
||||
message Request {
|
||||
// 用户名
|
||||
string username = 1;
|
||||
// 用户密码
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Response {
|
||||
// 用户名称
|
||||
string name = 1;
|
||||
// 用户性别
|
||||
string gender = 2;
|
||||
}
|
||||
|
||||
service User {
|
||||
// 登录
|
||||
rpc Login(Request)returns(Response);
|
||||
}
|
||||
```
|
||||
|
||||
* 生成rpc服务代码
|
||||
|
||||
```shell script
|
||||
goctl rpc proto -src=user.proto
|
||||
```
|
||||
|
||||
代码tree
|
||||
|
||||
```Plain Text
|
||||
user
|
||||
├── etc
|
||||
│ └── user.json
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── handler
|
||||
│ │ ├── loginhandler.go
|
||||
│ ├── logic
|
||||
│ │ └── loginlogic.go
|
||||
│ └── svc
|
||||
│ └── servicecontext.go
|
||||
├── pb
|
||||
│ └── user.pb.go
|
||||
├── shared
|
||||
│ ├── mockusermodel.go
|
||||
│ ├── types.go
|
||||
│ └── usermodel.go
|
||||
├── user.go
|
||||
└── user.proto
|
||||
|
||||
```
|
||||
|
||||
## 准备工作
|
||||
|
||||
* 安装了go环境
|
||||
* 安装了protoc&protoc-gen-go,并且已经设置环境变量
|
||||
* mockgen(可选,将移除)
|
||||
* 更多问题请见 <a href="#注意事项">注意事项</a>
|
||||
|
||||
## 用法
|
||||
|
||||
### rpc服务生成用法
|
||||
|
||||
```shell script
|
||||
goctl rpc proto -h
|
||||
```
|
||||
|
||||
```shell script
|
||||
NAME:
|
||||
goctl rpc proto - generate rpc from proto
|
||||
|
||||
USAGE:
|
||||
goctl rpc proto [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--src value, -s value the file path of the proto source file
|
||||
--dir value, -d value the target path of the code,default path is "${pwd}". [option]
|
||||
--service value, --srv value the name of rpc service. [option]
|
||||
--shared[已废弃] value the dir of the shared file,default path is "${pwd}/shared. [option]"
|
||||
--idea whether the command execution environment is from idea plugin. [option]
|
||||
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
* --src 必填,proto数据源,目前暂时支持单个proto文件生成,这里不支持(不建议)外部依赖
|
||||
* --dir 非必填,默认为proto文件所在目录,生成代码的目标目录
|
||||
* --service 服务名称,非必填,默认为proto文件所在目录名称,但是,如果proto所在目录为一下结构:
|
||||
|
||||
```shell script
|
||||
user
|
||||
├── cmd
|
||||
│ └── rpc
|
||||
│ └── user.proto
|
||||
```
|
||||
|
||||
则服务名称亦为user,而非proto所在文件夹名称了,这里推荐使用这种结构,可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
|
||||
* --shared[⚠️已废弃] 非必填,默认为$dir(xxx.proto)/shared,rpc client逻辑代码存放目录。
|
||||
|
||||
> 注意:这里的shared文件夹名称将会是代码中的package名称。
|
||||
|
||||
* --idea 非必填,是否为idea插件中执行,保留字段,终端执行可以忽略
|
||||
|
||||
## 开发人员需要做什么
|
||||
|
||||
关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开饭人员仅需要修改
|
||||
|
||||
* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
|
||||
* 服务中业务逻辑编写(internal/logic/xxlogic.go)
|
||||
* 服务中资源上下文的编写(internal/svc/servicecontext.go)
|
||||
|
||||
## 扩展
|
||||
|
||||
对于需要进行rpc mock的开发人员,在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。
|
||||
|
||||
## 注意事项
|
||||
|
||||
* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.2(see [https://github.com/grpc/grpc-go/issues/3347](https://github.com/grpc/grpc-go/issues/3347))即
|
||||
|
||||
```shell script
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
```
|
||||
|
||||
* proto不支持暂多文件同时生成
|
||||
* proto不支持外部依赖包引入,message不支持inline
|
||||
* 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有
|
||||
|
||||
```shell script
|
||||
// Code generated by goctl. DO NOT EDIT!
|
||||
// Source: xxx.proto
|
||||
```
|
||||
|
||||
的标识,请注意不要将也写业务性代码写在里面。
|
||||
|
||||
## 常见问题解决(go mod工程)
|
||||
|
||||
* 错误一:
|
||||
|
||||
```golang
|
||||
pb/xx.pb.go:220:7: undefined: grpc.ClientConnInterface
|
||||
pb/xx.pb.go:224:11: undefined: grpc.SupportPackageIsVersion6
|
||||
pb/xx.pb.go:234:5: undefined: grpc.ClientConnInterface
|
||||
pb/xx.pb.go:237:24: undefined: grpc.ClientConnInterface
|
||||
```
|
||||
|
||||
解决方法:请将`protoc-gen-go`版本降至v1.3.2及一下
|
||||
|
||||
* 错误二:
|
||||
|
||||
```golang
|
||||
|
||||
# go.etcd.io/etcd/clientv3/balancer/picker
|
||||
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/err.go:25:9: cannot use &errPicker literal (type *errPicker) as type Picker in return argument:*errPicker does not implement Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/roundrobin_balanced.go:33:9: cannot use &rrBalanced literal (type *rrBalanced) as type Picker in return argument:
|
||||
*rrBalanced does not implement Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
#github.com/tal-tech/go-zero/zrpc/internal/balancer/p2c
|
||||
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:41:32: not enough arguments in call to base.NewBalancerBuilder
|
||||
have (string, *p2cPickerBuilder)
|
||||
want (string, base.PickerBuilder, base.Config)
|
||||
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:58:9: cannot use &p2cPicker literal (type *p2cPicker) as type balancer.Picker in return argument:
|
||||
*p2cPicker does not implement balancer.Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
```
|
||||
|
||||
解决方法:
|
||||
|
||||
```golang
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
```
|
||||
299
doc/goctl.md
@@ -1,299 +0,0 @@
|
||||
# goctl使用
|
||||
|
||||
## goctl用途
|
||||
|
||||
* 定义api请求
|
||||
* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 晓程序),dart(flutter)
|
||||
* 生成MySQL CURD+Cache
|
||||
* 生成MongoDB CURD+Cache
|
||||
|
||||
## goctl使用说明
|
||||
|
||||
### 快速生成服务
|
||||
|
||||
* api: goctl api new xxxx
|
||||
* rpc: goctl rpc new xxxx
|
||||
|
||||
#### goctl参数说明
|
||||
|
||||
`goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
|
||||
|
||||
> api 后面接生成的语言,现支持go/java/typescript
|
||||
>
|
||||
> -api 自定义api所在路径
|
||||
>
|
||||
> -dir 自定义生成目录
|
||||
|
||||
#### 保持goctl总是最新版
|
||||
|
||||
第一次运行会在~/.goctl里增加下面两行:
|
||||
|
||||
```Plain Text
|
||||
url = http://47.97.184.41:7777/
|
||||
```
|
||||
|
||||
#### API 语法说明
|
||||
|
||||
``` golang
|
||||
info(
|
||||
title: doc title
|
||||
desc: >
|
||||
doc description first part,
|
||||
doc description second part<
|
||||
version: 1.0
|
||||
)
|
||||
|
||||
type int userType
|
||||
|
||||
type user struct {
|
||||
name string `json:"user"` // 用户姓名
|
||||
}
|
||||
|
||||
type student struct {
|
||||
name string `json:"name"` // 学生姓名
|
||||
}
|
||||
|
||||
type teacher struct {
|
||||
}
|
||||
|
||||
type (
|
||||
address struct {
|
||||
city string `json:"city"`
|
||||
}
|
||||
|
||||
innerType struct {
|
||||
image string `json:"image"`
|
||||
}
|
||||
|
||||
createRequest struct {
|
||||
innerType
|
||||
name string `form:"name"`
|
||||
age int `form:"age,optional"`
|
||||
address []address `json:"address,optional"`
|
||||
}
|
||||
|
||||
getRequest struct {
|
||||
name string `path:"name"`
|
||||
age int `form:"age,optional"`
|
||||
}
|
||||
|
||||
getResponse struct {
|
||||
code int `json:"code"`
|
||||
desc string `json:"desc,omitempty"`
|
||||
address address `json:"address"`
|
||||
service int `json:"service"`
|
||||
}
|
||||
)
|
||||
|
||||
service user-api {
|
||||
@doc(
|
||||
summary: user title
|
||||
desc: >
|
||||
user description first part,
|
||||
user description second part,
|
||||
user description second line
|
||||
)
|
||||
@server(
|
||||
handler: GetUserHandler
|
||||
group: user
|
||||
)
|
||||
get /api/user/:name(getRequest) returns(getResponse)
|
||||
|
||||
@server(
|
||||
handler: CreateUserHandler
|
||||
group: user
|
||||
)
|
||||
post /api/users/create(createRequest)
|
||||
}
|
||||
|
||||
@server(
|
||||
jwt: Auth
|
||||
group: profile
|
||||
)
|
||||
service user-api {
|
||||
@doc(summary: user title)
|
||||
@server(
|
||||
handler: GetProfileHandler
|
||||
)
|
||||
get /api/profile/:name(getRequest) returns(getResponse)
|
||||
|
||||
@server(
|
||||
handler: CreateProfileHandler
|
||||
)
|
||||
post /api/profile/create(createRequest)
|
||||
}
|
||||
|
||||
service user-api {
|
||||
@doc(summary: desc in one line)
|
||||
@server(
|
||||
handler: PingHandler
|
||||
)
|
||||
head /api/ping()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
1. info部分:描述了api基本信息,比如Auth,api是哪个用途。
|
||||
2. type部分:type类型声明和golang语法兼容。
|
||||
3. service部分:service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置jwt和auth认证,另外通过group属性可以指定service生成所在子目录。
|
||||
service里面包含api路由,比如上面第一组service的第一个路由,doc用来描述此路由的用途,GetProfileHandler表示处理这个路由的handler,
|
||||
`get /api/profile/:name(getRequest) returns(getResponse)` 中get代表api的请求方式(get/post/put/delete), `/api/profile/:name` 描述了路由path,`:name`通过
|
||||
请求getRequest里面的属性赋值,getResponse为返回的结构体,这两个类型都定义在2描述的类型中。
|
||||
|
||||
#### api vscode插件
|
||||
|
||||
开发者可以在vscode中搜索goctl的api插件,它提供了api语法高亮,语法检测和格式化相关功能。
|
||||
|
||||
1. 支持语法高亮和类型导航。
|
||||
2. 语法检测,格式化api会自动检测api编写错误地方,用vscode默认的格式化快捷键(option+command+F)或者自定义的也可以。
|
||||
3. 格式化(option+command+F),类似代码格式化,统一样式支持。
|
||||
|
||||
#### 根据定义好的api文件生成golang代码
|
||||
|
||||
命令如下:
|
||||
`goctl api go -api user/user.api -dir user`
|
||||
|
||||
```Plain Text
|
||||
|
||||
.
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── handler
|
||||
│ │ ├── pinghandler.go
|
||||
│ │ ├── profile
|
||||
│ │ │ ├── createprofilehandler.go
|
||||
│ │ │ └── getprofilehandler.go
|
||||
│ │ ├── routes.go
|
||||
│ │ └── user
|
||||
│ │ ├── createuserhandler.go
|
||||
│ │ └── getuserhandler.go
|
||||
│ ├── logic
|
||||
│ │ ├── pinglogic.go
|
||||
│ │ ├── profile
|
||||
│ │ │ ├── createprofilelogic.go
|
||||
│ │ │ └── getprofilelogic.go
|
||||
│ │ └── user
|
||||
│ │ ├── createuserlogic.go
|
||||
│ │ └── getuserlogic.go
|
||||
│ ├── svc
|
||||
│ │ └── servicecontext.go
|
||||
│ └── types
|
||||
│ └── types.go
|
||||
└── user.go
|
||||
|
||||
```
|
||||
|
||||
生成的代码可以直接跑,有几个地方需要改:
|
||||
|
||||
* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
|
||||
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
|
||||
|
||||
#### 根据定义好的api文件生成java代码
|
||||
|
||||
```shell
|
||||
goctl api java -api user/user.api -dir ./src
|
||||
```
|
||||
|
||||
#### 根据定义好的api文件生成typescript代码
|
||||
|
||||
```shell
|
||||
goctl api ts -api user/user.api -dir ./src -webapi ***
|
||||
|
||||
ts需要指定webapi所在目录
|
||||
```
|
||||
|
||||
#### 根据定义好的api文件生成Dart代码
|
||||
|
||||
```shell
|
||||
goctl api dart -api user/user.api -dir ./src
|
||||
```
|
||||
|
||||
## 根据mysql ddl或者datasource生成model文件
|
||||
|
||||
```shell script
|
||||
goctl model mysql -src={filename} -dir={dir} -cache={true|false}
|
||||
```
|
||||
|
||||
详情参考[model文档](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/model/sql/README.MD)
|
||||
|
||||
## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)
|
||||
|
||||
```shell
|
||||
goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes
|
||||
```
|
||||
|
||||
* src需要提供简单的usermodel.go文件,里面只需要提供一个结构体即可
|
||||
* cache 控制是否需要缓存 yes=需要 no=不需要
|
||||
|
||||
src 示例代码如下
|
||||
|
||||
```go
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
Name string `o:"find,get,set" c:"姓名"`
|
||||
Age int `o:"find,get,set" c:"年纪"`
|
||||
School string `c:"学校"`
|
||||
}
|
||||
```
|
||||
|
||||
结构体中不需要提供Id,CreateTime,UpdateTime三个字段,会自动生成
|
||||
结构体中每个tag有两个可选标签 c 和 o
|
||||
c 是该字段的注释
|
||||
o 是该字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
|
||||
生成的目标文件会覆盖该简单go文件
|
||||
|
||||
## goctl rpc生成(业务剥离中,暂未开放)
|
||||
|
||||
命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`
|
||||
如: `goctl rpc proto -proto test.proto -service test -project xjy -dir .`
|
||||
|
||||
参数说明:
|
||||
|
||||
* ${proto}: proto文件
|
||||
* ${serviceName}: rpc服务名称
|
||||
* ${projectName}: 所属项目,如xjy,xhb,crm,hera,具体查看help,主要为了根据不同项目服务往redis注册key,可选
|
||||
* ${directory}: 输出目录
|
||||
* ${shared}: shared文件生成目录,可选,默认为${pwd}/shared
|
||||
|
||||
生成目录结构示例:
|
||||
|
||||
```Plain Text
|
||||
.
|
||||
├── shared [示例目录,可自己指定,强制覆盖更新]
|
||||
│ └── contentservicemodel.go
|
||||
├── test
|
||||
│ ├── etc
|
||||
│ │ └── test.json
|
||||
│ ├── internal
|
||||
│ │ ├── config
|
||||
│ │ │ └── config.go
|
||||
│ │ ├── handler [强制覆盖更新]
|
||||
│ │ │ ├── changeavatarhandler.go
|
||||
│ │ │ ├── changebirthdayhandler.go
|
||||
│ │ │ ├── changenamehandler.go
|
||||
│ │ │ ├── changepasswordhandler.go
|
||||
│ │ │ ├── changeuserinfohandler.go
|
||||
│ │ │ ├── getuserinfohandler.go
|
||||
│ │ │ ├── loginhandler.go
|
||||
│ │ │ ├── logouthandler.go
|
||||
│ │ │ └── testhandler.go
|
||||
│ │ ├── logic
|
||||
│ │ │ ├── changeavatarlogic.go
|
||||
│ │ │ ├── changebirthdaylogic.go
|
||||
│ │ │ ├── changenamelogic.go
|
||||
│ │ │ ├── changepasswordlogic.go
|
||||
│ │ │ ├── changeuserinfologic.go
|
||||
│ │ │ ├── getuserinfologic.go
|
||||
│ │ │ ├── loginlogic.go
|
||||
│ │ │ └── logoutlogic.go
|
||||
│ │ └── svc
|
||||
│ │ └── servicecontext.go
|
||||
│ ├── pb
|
||||
│ │ └── test.pb.go
|
||||
│ └── test.go [强制覆盖更新]
|
||||
└── test.proto
|
||||
```
|
||||
|
||||
注意 :目前rpc目录生成的proto文件暂不支持import外部proto文件
|
||||
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 72 KiB |
BIN
doc/images/go-zero.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 457 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 91 KiB |