mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-26 16:15:30 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ff6a10d3 | ||
|
|
a71e56de52 | ||
|
|
bae8d4f4c8 | ||
|
|
8c6266f338 | ||
|
|
95d5b81f44 | ||
|
|
bca7bbc142 | ||
|
|
df9a52664b | ||
|
|
937cf0db96 | ||
|
|
75cebb65f8 | ||
|
|
410f56e73a | ||
|
|
017909a3ab | ||
|
|
0d31e6c375 | ||
|
|
0ba86b1849 | ||
|
|
4cacc4d9d3 | ||
|
|
a99c14da4a | ||
|
|
985582264a | ||
|
|
8364e341e1 |
@@ -423,7 +423,7 @@ func TestRegistry_Monitor(t *testing.T) {
|
|||||||
GetRegistry().clusters = map[string]*cluster{
|
GetRegistry().clusters = map[string]*cluster{
|
||||||
getClusterKey(endpoints): {
|
getClusterKey(endpoints): {
|
||||||
watchers: map[watchKey]*watchValue{
|
watchers: map[watchKey]*watchValue{
|
||||||
watchKey{
|
{
|
||||||
key: "foo",
|
key: "foo",
|
||||||
exactMatch: true,
|
exactMatch: true,
|
||||||
}: {
|
}: {
|
||||||
@@ -449,7 +449,7 @@ func TestRegistry_Unmonitor(t *testing.T) {
|
|||||||
GetRegistry().clusters = map[string]*cluster{
|
GetRegistry().clusters = map[string]*cluster{
|
||||||
getClusterKey(endpoints): {
|
getClusterKey(endpoints): {
|
||||||
watchers: map[watchKey]*watchValue{
|
watchers: map[watchKey]*watchValue{
|
||||||
watchKey{
|
{
|
||||||
key: "foo",
|
key: "foo",
|
||||||
exactMatch: true,
|
exactMatch: true,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fieldsContextKey contextKey
|
|
||||||
globalFields atomic.Value
|
globalFields atomic.Value
|
||||||
globalFieldsLock sync.Mutex
|
globalFieldsLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey struct{}
|
type fieldsKey struct{}
|
||||||
|
|
||||||
// AddGlobalFields adds global fields.
|
// AddGlobalFields adds global fields.
|
||||||
func AddGlobalFields(fields ...LogField) {
|
func AddGlobalFields(fields ...LogField) {
|
||||||
@@ -29,16 +28,16 @@ func AddGlobalFields(fields ...LogField) {
|
|||||||
|
|
||||||
// ContextWithFields returns a new context with the given fields.
|
// ContextWithFields returns a new context with the given fields.
|
||||||
func ContextWithFields(ctx context.Context, fields ...LogField) context.Context {
|
func ContextWithFields(ctx context.Context, fields ...LogField) context.Context {
|
||||||
if val := ctx.Value(fieldsContextKey); val != nil {
|
if val := ctx.Value(fieldsKey{}); val != nil {
|
||||||
if arr, ok := val.([]LogField); ok {
|
if arr, ok := val.([]LogField); ok {
|
||||||
allFields := make([]LogField, 0, len(arr)+len(fields))
|
allFields := make([]LogField, 0, len(arr)+len(fields))
|
||||||
allFields = append(allFields, arr...)
|
allFields = append(allFields, arr...)
|
||||||
allFields = append(allFields, fields...)
|
allFields = append(allFields, fields...)
|
||||||
return context.WithValue(ctx, fieldsContextKey, allFields)
|
return context.WithValue(ctx, fieldsKey{}, allFields)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.WithValue(ctx, fieldsContextKey, fields)
|
return context.WithValue(ctx, fieldsKey{}, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFields returns a new logger with the given fields.
|
// WithFields returns a new logger with the given fields.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestAddGlobalFields(t *testing.T) {
|
|||||||
|
|
||||||
func TestContextWithFields(t *testing.T) {
|
func TestContextWithFields(t *testing.T) {
|
||||||
ctx := ContextWithFields(context.Background(), Field("a", 1), Field("b", 2))
|
ctx := ContextWithFields(context.Background(), Field("a", 1), Field("b", 2))
|
||||||
vals := ctx.Value(fieldsContextKey)
|
vals := ctx.Value(fieldsKey{})
|
||||||
assert.NotNil(t, vals)
|
assert.NotNil(t, vals)
|
||||||
fields, ok := vals.([]LogField)
|
fields, ok := vals.([]LogField)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
@@ -43,7 +43,7 @@ func TestContextWithFields(t *testing.T) {
|
|||||||
|
|
||||||
func TestWithFields(t *testing.T) {
|
func TestWithFields(t *testing.T) {
|
||||||
ctx := WithFields(context.Background(), Field("a", 1), Field("b", 2))
|
ctx := WithFields(context.Background(), Field("a", 1), Field("b", 2))
|
||||||
vals := ctx.Value(fieldsContextKey)
|
vals := ctx.Value(fieldsKey{})
|
||||||
assert.NotNil(t, vals)
|
assert.NotNil(t, vals)
|
||||||
fields, ok := vals.([]LogField)
|
fields, ok := vals.([]LogField)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
@@ -55,7 +55,7 @@ func TestWithFieldsAppend(t *testing.T) {
|
|||||||
ctx := context.WithValue(context.Background(), dummyKey, "dummy")
|
ctx := context.WithValue(context.Background(), dummyKey, "dummy")
|
||||||
ctx = ContextWithFields(ctx, Field("a", 1), Field("b", 2))
|
ctx = ContextWithFields(ctx, Field("a", 1), Field("b", 2))
|
||||||
ctx = ContextWithFields(ctx, Field("c", 3), Field("d", 4))
|
ctx = ContextWithFields(ctx, Field("c", 3), Field("d", 4))
|
||||||
vals := ctx.Value(fieldsContextKey)
|
vals := ctx.Value(fieldsKey{})
|
||||||
assert.NotNil(t, vals)
|
assert.NotNil(t, vals)
|
||||||
fields, ok := vals.([]LogField)
|
fields, ok := vals.([]LogField)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
@@ -80,8 +80,8 @@ func TestWithFieldsAppendCopy(t *testing.T) {
|
|||||||
ctxa := ContextWithFields(ctx, af)
|
ctxa := ContextWithFields(ctx, af)
|
||||||
ctxb := ContextWithFields(ctx, bf)
|
ctxb := ContextWithFields(ctx, bf)
|
||||||
|
|
||||||
assert.EqualValues(t, af, ctxa.Value(fieldsContextKey).([]LogField)[count])
|
assert.EqualValues(t, af, ctxa.Value(fieldsKey{}).([]LogField)[count])
|
||||||
assert.EqualValues(t, bf, ctxb.Value(fieldsContextKey).([]LogField)[count])
|
assert.EqualValues(t, bf, ctxb.Value(fieldsKey{}).([]LogField)[count])
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkAtomicValue(b *testing.B) {
|
func BenchmarkAtomicValue(b *testing.B) {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func (l *richLogger) buildFields(fields ...LogField) []LogField {
|
|||||||
fields = append(fields, Field(spanKey, spanID))
|
fields = append(fields, Field(spanKey, spanID))
|
||||||
}
|
}
|
||||||
|
|
||||||
val := l.ctx.Value(fieldsContextKey)
|
val := l.ctx.Value(fieldsKey{})
|
||||||
if val != nil {
|
if val != nil {
|
||||||
if arr, ok := val.([]LogField); ok {
|
if arr, ok := val.([]LogField); ok {
|
||||||
fields = append(fields, arr...)
|
fields = append(fields, arr...)
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ var (
|
|||||||
errValueNotSettable = errors.New("value is not settable")
|
errValueNotSettable = errors.New("value is not settable")
|
||||||
errValueNotStruct = errors.New("value type is not struct")
|
errValueNotStruct = errors.New("value type is not struct")
|
||||||
keyUnmarshaler = NewUnmarshaler(defaultKeyName)
|
keyUnmarshaler = NewUnmarshaler(defaultKeyName)
|
||||||
|
boolType = reflect.TypeOf(false)
|
||||||
durationType = reflect.TypeOf(time.Duration(0))
|
durationType = reflect.TypeOf(time.Duration(0))
|
||||||
|
stringType = reflect.TypeOf("")
|
||||||
cacheKeys = make(map[string][]string)
|
cacheKeys = make(map[string][]string)
|
||||||
cacheKeysLock sync.Mutex
|
cacheKeysLock sync.Mutex
|
||||||
defaultCache = make(map[string]any)
|
defaultCache = make(map[string]any)
|
||||||
@@ -622,9 +624,19 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
|
|||||||
|
|
||||||
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
|
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
|
||||||
case valueKind == reflect.String && derefedFieldType == durationType:
|
case valueKind == reflect.String && derefedFieldType == durationType:
|
||||||
return fillDurationValue(fieldType, value, mapValue.(string))
|
v, err := convertToString(mapValue, fullName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fillDurationValue(fieldType, value, v)
|
||||||
case valueKind == reflect.String && typeKind == reflect.Struct && u.implementsUnmarshaler(fieldType):
|
case valueKind == reflect.String && typeKind == reflect.Struct && u.implementsUnmarshaler(fieldType):
|
||||||
return u.fillUnmarshalerStruct(fieldType, value, mapValue.(string))
|
v, err := convertToString(mapValue, fullName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fillUnmarshalerStruct(fieldType, value, v)
|
||||||
default:
|
default:
|
||||||
return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName)
|
return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName)
|
||||||
}
|
}
|
||||||
@@ -755,24 +767,24 @@ func (u *Unmarshaler) processFieldWithEnvValue(fieldType reflect.Type, value ref
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldKind := fieldType.Kind()
|
derefType := Deref(fieldType)
|
||||||
switch fieldKind {
|
switch derefType {
|
||||||
case reflect.Bool:
|
case boolType:
|
||||||
val, err := strconv.ParseBool(envVal)
|
val, err := strconv.ParseBool(envVal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unmarshal field %q with environment variable, %w", fullName, err)
|
return fmt.Errorf("unmarshal field %q with environment variable, %w", fullName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
value.SetBool(val)
|
SetValue(fieldType, value, reflect.ValueOf(val))
|
||||||
return nil
|
return nil
|
||||||
case durationType.Kind():
|
case durationType:
|
||||||
if err := fillDurationValue(fieldType, value, envVal); err != nil {
|
if err := fillDurationValue(fieldType, value, envVal); err != nil {
|
||||||
return fmt.Errorf("unmarshal field %q with environment variable, %w", fullName, err)
|
return fmt.Errorf("unmarshal field %q with environment variable, %w", fullName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case reflect.String:
|
case stringType:
|
||||||
value.SetString(envVal)
|
SetValue(fieldType, value, reflect.ValueOf(envVal))
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return u.processFieldPrimitiveWithJSONNumber(fieldType, value, json.Number(envVal), opts, fullName)
|
return u.processFieldPrimitiveWithJSONNumber(fieldType, value, json.Number(envVal), opts, fullName)
|
||||||
|
|||||||
@@ -203,6 +203,20 @@ func TestUnmarshalDuration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalDurationUnexpectedError(t *testing.T) {
|
||||||
|
type inner struct {
|
||||||
|
Duration time.Duration `key:"duration"`
|
||||||
|
}
|
||||||
|
content := "{\"duration\": 1}"
|
||||||
|
var m = map[string]any{}
|
||||||
|
err := jsonx.Unmarshal([]byte(content), &m)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var in inner
|
||||||
|
err = UnmarshalKey(m, &in)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "expect string")
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnmarshalDurationDefault(t *testing.T) {
|
func TestUnmarshalDurationDefault(t *testing.T) {
|
||||||
type inner struct {
|
type inner struct {
|
||||||
Int int `key:"int"`
|
Int int `key:"int"`
|
||||||
@@ -4665,6 +4679,23 @@ func TestUnmarshal_EnvInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshal_EnvInt64(t *testing.T) {
|
||||||
|
type Value struct {
|
||||||
|
Age int64 `key:"age,env=TEST_NAME_INT64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
envName = "TEST_NAME_INT64"
|
||||||
|
envVal = "88"
|
||||||
|
)
|
||||||
|
t.Setenv(envName, envVal)
|
||||||
|
|
||||||
|
var v Value
|
||||||
|
if assert.NoError(t, UnmarshalKey(emptyMap, &v)) {
|
||||||
|
assert.Equal(t, int64(88), v.Age)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnmarshal_EnvIntOverwrite(t *testing.T) {
|
func TestUnmarshal_EnvIntOverwrite(t *testing.T) {
|
||||||
type Value struct {
|
type Value struct {
|
||||||
Age int `key:"age,env=TEST_NAME_INT"`
|
Age int `key:"age,env=TEST_NAME_INT"`
|
||||||
@@ -4770,20 +4801,33 @@ func TestUnmarshal_EnvBoolBad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshal_EnvDuration(t *testing.T) {
|
func TestUnmarshal_EnvDuration(t *testing.T) {
|
||||||
type Value struct {
|
|
||||||
Duration time.Duration `key:"duration,env=TEST_NAME_DURATION"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
envName = "TEST_NAME_DURATION"
|
envName = "TEST_NAME_DURATION"
|
||||||
envVal = "1s"
|
envVal = "1s"
|
||||||
)
|
)
|
||||||
t.Setenv(envName, envVal)
|
t.Setenv(envName, envVal)
|
||||||
|
|
||||||
var v Value
|
t.Run("valid duration", func(t *testing.T) {
|
||||||
if assert.NoError(t, UnmarshalKey(emptyMap, &v)) {
|
type Value struct {
|
||||||
assert.Equal(t, time.Second, v.Duration)
|
Duration time.Duration `key:"duration,env=TEST_NAME_DURATION"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var v Value
|
||||||
|
if assert.NoError(t, UnmarshalKey(emptyMap, &v)) {
|
||||||
|
assert.Equal(t, time.Second, v.Duration)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ptr of duration", func(t *testing.T) {
|
||||||
|
type Value struct {
|
||||||
|
Duration *time.Duration `key:"duration,env=TEST_NAME_DURATION"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var v Value
|
||||||
|
if assert.NoError(t, UnmarshalKey(emptyMap, &v)) {
|
||||||
|
assert.Equal(t, time.Second, *v.Duration)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshal_EnvDurationBadValue(t *testing.T) {
|
func TestUnmarshal_EnvDurationBadValue(t *testing.T) {
|
||||||
@@ -5995,6 +6039,16 @@ func TestUnmarshal_Unmarshaler(t *testing.T) {
|
|||||||
}, &v))
|
}, &v))
|
||||||
assert.Nil(t, v.Foo)
|
assert.Nil(t, v.Foo)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("json.Number", func(t *testing.T) {
|
||||||
|
v := struct {
|
||||||
|
Foo *mockUnmarshaler `json:"name"`
|
||||||
|
}{}
|
||||||
|
m := map[string]any{
|
||||||
|
"name": json.Number("123"),
|
||||||
|
}
|
||||||
|
assert.Error(t, UnmarshalJsonMap(m, &v))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseJsonStringValue(t *testing.T) {
|
func TestParseJsonStringValue(t *testing.T) {
|
||||||
|
|||||||
@@ -92,6 +92,15 @@ func ValidatePtr(v reflect.Value) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertToString(val any, fullName string) (string, error) {
|
||||||
|
v, ok := val.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("expect string for field %s, but got type %T", fullName, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertTypeFromString(kind reflect.Kind, str string) (any, error) {
|
func convertTypeFromString(kind reflect.Kind, str string) (any, error) {
|
||||||
switch kind {
|
switch kind {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
|
|||||||
29
core/stores/sqlx/config.go
Normal file
29
core/stores/sqlx/config.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package sqlx
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
errEmptyDatasource = errors.New("empty datasource")
|
||||||
|
errEmptyDriverName = errors.New("empty driver name")
|
||||||
|
)
|
||||||
|
|
||||||
|
// SqlConf defines the configuration for sqlx.
|
||||||
|
type SqlConf struct {
|
||||||
|
DataSource string
|
||||||
|
DriverName string `json:",default=mysql"`
|
||||||
|
Replicas []string `json:",optional"`
|
||||||
|
Policy string `json:",default=round-robin,options=round-robin|random"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the SqlxConf.
|
||||||
|
func (sc SqlConf) Validate() error {
|
||||||
|
if len(sc.DataSource) == 0 {
|
||||||
|
return errEmptyDatasource
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sc.DriverName) == 0 {
|
||||||
|
return errEmptyDriverName
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
29
core/stores/sqlx/config_test.go
Normal file
29
core/stores/sqlx/config_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package sqlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
text := []byte(`DataSource: primary:password@tcp(127.0.0.1:3306)/primary_db
|
||||||
|
`)
|
||||||
|
|
||||||
|
var sc SqlConf
|
||||||
|
err := conf.LoadFromYamlBytes(text, &sc)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "mysql", sc.DriverName)
|
||||||
|
assert.Equal(t, policyRoundRobin, sc.Policy)
|
||||||
|
assert.Nil(t, sc.Validate())
|
||||||
|
|
||||||
|
sc = SqlConf{}
|
||||||
|
assert.Equal(t, errEmptyDatasource, sc.Validate())
|
||||||
|
|
||||||
|
sc.DataSource = "primary:password@tcp(127.0.0.1:3306)/primary_db"
|
||||||
|
assert.Equal(t, errEmptyDriverName, sc.Validate())
|
||||||
|
|
||||||
|
sc.DriverName = "mysql"
|
||||||
|
assert.Nil(t, sc.Validate())
|
||||||
|
}
|
||||||
65
core/stores/sqlx/rwstrategy.go
Normal file
65
core/stores/sqlx/rwstrategy.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package sqlx
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// policyRoundRobin round-robin policy for selecting replicas.
|
||||||
|
policyRoundRobin = "round-robin"
|
||||||
|
// policyRandom random policy for selecting replicas.
|
||||||
|
policyRandom = "random"
|
||||||
|
|
||||||
|
// readPrimaryMode indicates that the operation is a read,
|
||||||
|
// but should be performed on the primary database instance.
|
||||||
|
//
|
||||||
|
// This mode is used in scenarios where data freshness and consistency are critical,
|
||||||
|
// such as immediately after writes or where replication lag may cause stale reads.
|
||||||
|
readPrimaryMode readWriteMode = "read-primary"
|
||||||
|
|
||||||
|
// readReplicaMode indicates that the operation is a read from replicas.
|
||||||
|
// This is suitable for scenarios where eventual consistency is acceptable,
|
||||||
|
// and the goal is to offload traffic from the primary and improve read scalability.
|
||||||
|
readReplicaMode readWriteMode = "read-replica"
|
||||||
|
|
||||||
|
// writeMode indicates that the operation is a write operation (to primary).
|
||||||
|
writeMode readWriteMode = "write"
|
||||||
|
|
||||||
|
// notSpecifiedMode indicates that the read/write mode is not specified.
|
||||||
|
notSpecifiedMode readWriteMode = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type readWriteModeKey struct{}
|
||||||
|
|
||||||
|
// WithReadPrimary sets the context to read-primary mode.
|
||||||
|
func WithReadPrimary(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, readWriteModeKey{}, readPrimaryMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReadReplica sets the context to read-replica mode.
|
||||||
|
func WithReadReplica(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, readWriteModeKey{}, readReplicaMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWrite sets the context to write mode, indicating that the operation is a write operation.
|
||||||
|
func WithWrite(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, readWriteModeKey{}, writeMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type readWriteMode string
|
||||||
|
|
||||||
|
func (m readWriteMode) isValid() bool {
|
||||||
|
return m == readPrimaryMode || m == readReplicaMode || m == writeMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReadWriteMode(ctx context.Context) readWriteMode {
|
||||||
|
if mode := ctx.Value(readWriteModeKey{}); mode != nil {
|
||||||
|
if v, ok := mode.(readWriteMode); ok && v.isValid() {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notSpecifiedMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func usePrimary(ctx context.Context) bool {
|
||||||
|
return getReadWriteMode(ctx) != readReplicaMode
|
||||||
|
}
|
||||||
142
core/stores/sqlx/rwstrategy_test.go
Normal file
142
core/stores/sqlx/rwstrategy_test.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package sqlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsValid(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
mode readWriteMode
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid read-primary mode",
|
||||||
|
mode: readPrimaryMode,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid read-replica mode",
|
||||||
|
mode: readReplicaMode,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid write mode",
|
||||||
|
mode: writeMode,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not specified mode (empty)",
|
||||||
|
mode: notSpecifiedMode,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid custom string",
|
||||||
|
mode: readWriteMode("delete"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case sensitive check",
|
||||||
|
mode: readWriteMode("READ"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
actual := tc.mode.isValid()
|
||||||
|
assert.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithReadMode(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
readPrimaryCtx := WithReadPrimary(ctx)
|
||||||
|
|
||||||
|
val := readPrimaryCtx.Value(readWriteModeKey{})
|
||||||
|
assert.Equal(t, readPrimaryMode, val)
|
||||||
|
|
||||||
|
readReplicaCtx := WithReadReplica(ctx)
|
||||||
|
val = readReplicaCtx.Value(readWriteModeKey{})
|
||||||
|
assert.Equal(t, readReplicaMode, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithWriteMode(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
writeCtx := WithWrite(ctx)
|
||||||
|
|
||||||
|
val := writeCtx.Value(readWriteModeKey{})
|
||||||
|
assert.Equal(t, writeMode, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetReadWriteMode(t *testing.T) {
|
||||||
|
t.Run("valid read-primary mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, readPrimaryMode)
|
||||||
|
assert.Equal(t, readPrimaryMode, getReadWriteMode(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid read-replica mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, readReplicaMode)
|
||||||
|
assert.Equal(t, readReplicaMode, getReadWriteMode(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid write mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, writeMode)
|
||||||
|
assert.Equal(t, writeMode, getReadWriteMode(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid mode value (wrong type)", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, "not-a-mode")
|
||||||
|
assert.Equal(t, notSpecifiedMode, getReadWriteMode(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid mode value (wrong value)", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, readWriteMode("delete"))
|
||||||
|
assert.Equal(t, notSpecifiedMode, getReadWriteMode(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no mode set", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.Equal(t, notSpecifiedMode, getReadWriteMode(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsePrimary(t *testing.T) {
|
||||||
|
t.Run("context with read-replica mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, readReplicaMode)
|
||||||
|
assert.False(t, usePrimary(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("context with read-primary mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, readPrimaryMode)
|
||||||
|
assert.True(t, usePrimary(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("context with write mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, writeMode)
|
||||||
|
assert.True(t, usePrimary(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("context with invalid mode", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), readWriteModeKey{}, readWriteMode("invalid"))
|
||||||
|
assert.True(t, usePrimary(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("context with no mode set", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.True(t, usePrimary(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithModeTwice(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = WithReadPrimary(ctx)
|
||||||
|
writeCtx := WithWrite(ctx)
|
||||||
|
|
||||||
|
val := writeCtx.Value(readWriteModeKey{})
|
||||||
|
assert.Equal(t, writeMode, val)
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/breaker"
|
"github.com/zeromicro/go-zero/core/breaker"
|
||||||
"github.com/zeromicro/go-zero/core/errorx"
|
"github.com/zeromicro/go-zero/core/errorx"
|
||||||
@@ -52,9 +55,10 @@ type (
|
|||||||
beginTx beginnable
|
beginTx beginnable
|
||||||
brk breaker.Breaker
|
brk breaker.Breaker
|
||||||
accept breaker.Acceptable
|
accept breaker.Acceptable
|
||||||
|
index uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
connProvider func() (*sql.DB, error)
|
connProvider func(ctx context.Context) (*sql.DB, error)
|
||||||
|
|
||||||
sessionConn interface {
|
sessionConn interface {
|
||||||
Exec(query string, args ...any) (sql.Result, error)
|
Exec(query string, args ...any) (sql.Result, error)
|
||||||
@@ -64,10 +68,41 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MustNewConn returns a SqlConn with the given SqlConf.
|
||||||
|
func MustNewConn(c SqlConf, opts ...SqlOption) SqlConn {
|
||||||
|
conn, err := NewConn(c, opts...)
|
||||||
|
if err != nil {
|
||||||
|
logx.Must(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn returns a SqlConn with the given SqlConf.
|
||||||
|
func NewConn(c SqlConf, opts ...SqlOption) (SqlConn, error) {
|
||||||
|
if err := c.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &commonSqlConn{
|
||||||
|
onError: func(ctx context.Context, err error) {
|
||||||
|
logInstanceError(ctx, c.DataSource, err)
|
||||||
|
},
|
||||||
|
beginTx: begin,
|
||||||
|
brk: breaker.NewBreaker(),
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(conn)
|
||||||
|
}
|
||||||
|
conn.connProv = getConnProvider(conn, c.DriverName, c.DataSource, c.Policy, c.Replicas)
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewSqlConn returns a SqlConn with given driver name and datasource.
|
// NewSqlConn returns a SqlConn with given driver name and datasource.
|
||||||
func NewSqlConn(driverName, datasource string, opts ...SqlOption) SqlConn {
|
func NewSqlConn(driverName, datasource string, opts ...SqlOption) SqlConn {
|
||||||
conn := &commonSqlConn{
|
conn := &commonSqlConn{
|
||||||
connProv: func() (*sql.DB, error) {
|
connProv: func(context.Context) (*sql.DB, error) {
|
||||||
return getSqlConn(driverName, datasource)
|
return getSqlConn(driverName, datasource)
|
||||||
},
|
},
|
||||||
onError: func(ctx context.Context, err error) {
|
onError: func(ctx context.Context, err error) {
|
||||||
@@ -87,7 +122,7 @@ func NewSqlConn(driverName, datasource string, opts ...SqlOption) SqlConn {
|
|||||||
// Use it with caution; it's provided for other ORM to interact with.
|
// Use it with caution; it's provided for other ORM to interact with.
|
||||||
func NewSqlConnFromDB(db *sql.DB, opts ...SqlOption) SqlConn {
|
func NewSqlConnFromDB(db *sql.DB, opts ...SqlOption) SqlConn {
|
||||||
conn := &commonSqlConn{
|
conn := &commonSqlConn{
|
||||||
connProv: func() (*sql.DB, error) {
|
connProv: func(ctx context.Context) (*sql.DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
},
|
},
|
||||||
onError: func(ctx context.Context, err error) {
|
onError: func(ctx context.Context, err error) {
|
||||||
@@ -123,7 +158,7 @@ func (db *commonSqlConn) ExecCtx(ctx context.Context, q string, args ...any) (
|
|||||||
|
|
||||||
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
|
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
|
||||||
var conn *sql.DB
|
var conn *sql.DB
|
||||||
conn, err = db.connProv()
|
conn, err = db.connProv(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.onError(ctx, err)
|
db.onError(ctx, err)
|
||||||
return err
|
return err
|
||||||
@@ -151,7 +186,7 @@ func (db *commonSqlConn) PrepareCtx(ctx context.Context, query string) (stmt Stm
|
|||||||
|
|
||||||
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
|
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
|
||||||
var conn *sql.DB
|
var conn *sql.DB
|
||||||
conn, err = db.connProv()
|
conn, err = db.connProv(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.onError(ctx, err)
|
db.onError(ctx, err)
|
||||||
return err
|
return err
|
||||||
@@ -242,7 +277,7 @@ func (db *commonSqlConn) QueryRowsPartialCtx(ctx context.Context, v any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *commonSqlConn) RawDB() (*sql.DB, error) {
|
func (db *commonSqlConn) RawDB() (*sql.DB, error) {
|
||||||
return db.connProv()
|
return db.connProv(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *commonSqlConn) Transact(fn func(Session) error) error {
|
func (db *commonSqlConn) Transact(fn func(Session) error) error {
|
||||||
@@ -288,7 +323,7 @@ func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows)
|
|||||||
q string, args ...any) (err error) {
|
q string, args ...any) (err error) {
|
||||||
var scanFailed bool
|
var scanFailed bool
|
||||||
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
|
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
|
||||||
conn, err := db.connProv()
|
conn, err := db.connProv(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.onError(ctx, err)
|
db.onError(ctx, err)
|
||||||
return err
|
return err
|
||||||
@@ -311,6 +346,38 @@ func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getConnProvider(sc *commonSqlConn, driverName, datasource, policy string, replicas []string) connProvider {
|
||||||
|
return func(ctx context.Context) (*sql.DB, error) {
|
||||||
|
replicaCount := len(replicas)
|
||||||
|
|
||||||
|
if replicaCount == 0 || usePrimary(ctx) {
|
||||||
|
return getSqlConn(driverName, datasource)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dsn string
|
||||||
|
|
||||||
|
if replicaCount == 1 {
|
||||||
|
dsn = replicas[0]
|
||||||
|
} else {
|
||||||
|
if len(policy) == 0 {
|
||||||
|
policy = policyRoundRobin
|
||||||
|
}
|
||||||
|
|
||||||
|
switch policy {
|
||||||
|
case policyRandom:
|
||||||
|
dsn = replicas[rand.Intn(replicaCount)]
|
||||||
|
case policyRoundRobin:
|
||||||
|
index := atomic.AddUint32(&sc.index, 1) - 1
|
||||||
|
dsn = replicas[index%uint32(replicaCount)]
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown policy: %s", policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSqlConn(driverName, dsn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithAcceptable returns a SqlOption that setting the acceptable function.
|
// WithAcceptable returns a SqlOption that setting the acceptable function.
|
||||||
// acceptable is the func to check if the error can be accepted.
|
// acceptable is the func to check if the error can be accepted.
|
||||||
func WithAcceptable(acceptable func(err error) bool) SqlOption {
|
func WithAcceptable(acceptable func(err error) bool) SqlOption {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package sqlx
|
package sqlx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@@ -98,7 +99,7 @@ func TestSqlConn_RawDB(t *testing.T) {
|
|||||||
func TestSqlConn_Errors(t *testing.T) {
|
func TestSqlConn_Errors(t *testing.T) {
|
||||||
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
||||||
conn := NewSqlConnFromDB(db)
|
conn := NewSqlConnFromDB(db)
|
||||||
conn.(*commonSqlConn).connProv = func() (*sql.DB, error) {
|
conn.(*commonSqlConn).connProv = func(ctx context.Context) (*sql.DB, error) {
|
||||||
return nil, errors.New("error")
|
return nil, errors.New("error")
|
||||||
}
|
}
|
||||||
_, err := conn.Prepare("any")
|
_, err := conn.Prepare("any")
|
||||||
@@ -138,6 +139,148 @@ func TestSqlConn_Errors(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigSqlConn(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
assert.NotNil(t, db)
|
||||||
|
assert.NotNil(t, mock)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
connManager.Inject(mockedDatasource, db)
|
||||||
|
mock.ExpectExec("any")
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"foo"}))
|
||||||
|
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf, withMysqlAcceptable())
|
||||||
|
|
||||||
|
_, err = conn.Exec("any", "value")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
_, err = conn.Prepare("any")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
var val string
|
||||||
|
assert.NotNil(t, conn.QueryRow(&val, "any"))
|
||||||
|
assert.NotNil(t, conn.QueryRowPartial(&val, "any"))
|
||||||
|
assert.NotNil(t, conn.QueryRows(&val, "any"))
|
||||||
|
assert.NotNil(t, conn.QueryRowsPartial(&val, "any"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSqlConnStatement(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
assert.NotNil(t, db)
|
||||||
|
assert.NotNil(t, mock)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
connManager.Inject(mockedDatasource, db)
|
||||||
|
|
||||||
|
mock.ExpectPrepare("any")
|
||||||
|
mock.ExpectExec("any").WillReturnResult(sqlmock.NewResult(2, 3))
|
||||||
|
mock.ExpectPrepare("any")
|
||||||
|
row := sqlmock.NewRows([]string{"foo"}).AddRow("bar")
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(row)
|
||||||
|
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf, withMysqlAcceptable())
|
||||||
|
stmt, err := conn.Prepare("any")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := stmt.Exec()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
lastInsertID, err := res.LastInsertId()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(2), lastInsertID)
|
||||||
|
rowsAffected, err := res.RowsAffected()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(3), rowsAffected)
|
||||||
|
|
||||||
|
stmt, err = conn.Prepare("any")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var val string
|
||||||
|
err = stmt.QueryRow(&val)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar", val)
|
||||||
|
|
||||||
|
mock.ExpectPrepare("any")
|
||||||
|
rows := sqlmock.NewRows([]string{"any"}).AddRow("foo").AddRow("bar")
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(rows)
|
||||||
|
|
||||||
|
stmt, err = conn.Prepare("any")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var vals []string
|
||||||
|
assert.NoError(t, stmt.QueryRowsPartial(&vals))
|
||||||
|
assert.ElementsMatch(t, []string{"foo", "bar"}, vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSqlConnQuery(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
assert.NotNil(t, db)
|
||||||
|
assert.NotNil(t, mock)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
connManager.Inject(mockedDatasource, db)
|
||||||
|
|
||||||
|
t.Run("QueryRow", func(t *testing.T) {
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"foo"}).AddRow("bar"))
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf)
|
||||||
|
var val string
|
||||||
|
assert.NoError(t, conn.QueryRow(&val, "any"))
|
||||||
|
assert.Equal(t, "bar", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("QueryRowPartial", func(t *testing.T) {
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"foo"}).AddRow("bar"))
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf)
|
||||||
|
var val string
|
||||||
|
assert.NoError(t, conn.QueryRowPartial(&val, "any"))
|
||||||
|
assert.Equal(t, "bar", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("QueryRows", func(t *testing.T) {
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"any"}).AddRow("foo").AddRow("bar"))
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf)
|
||||||
|
var vals []string
|
||||||
|
assert.NoError(t, conn.QueryRows(&vals, "any"))
|
||||||
|
assert.ElementsMatch(t, []string{"foo", "bar"}, vals)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("QueryRowsPartial", func(t *testing.T) {
|
||||||
|
mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"any"}).AddRow("foo").AddRow("bar"))
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf)
|
||||||
|
var vals []string
|
||||||
|
assert.NoError(t, conn.QueryRowsPartial(&vals, "any"))
|
||||||
|
assert.ElementsMatch(t, []string{"foo", "bar"}, vals)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSqlConnErr(t *testing.T) {
|
||||||
|
t.Run("panic on empty config", func(t *testing.T) {
|
||||||
|
original := logx.ExitOnFatal.True()
|
||||||
|
logx.ExitOnFatal.Set(false)
|
||||||
|
defer logx.ExitOnFatal.Set(original)
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
MustNewConn(SqlConf{})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("on error", func(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
assert.NotNil(t, db)
|
||||||
|
assert.NotNil(t, mock)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
connManager.Inject(mockedDatasource, db)
|
||||||
|
|
||||||
|
conf := SqlConf{DataSource: mockedDatasource, DriverName: mysqlDriverName}
|
||||||
|
conn := MustNewConn(conf)
|
||||||
|
conn.(*commonSqlConn).connProv = func(ctx context.Context) (*sql.DB, error) {
|
||||||
|
return nil, errors.New("error")
|
||||||
|
}
|
||||||
|
_, err = conn.Prepare("any")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStatement(t *testing.T) {
|
func TestStatement(t *testing.T) {
|
||||||
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
||||||
mock.ExpectPrepare("any").WillBeClosed()
|
mock.ExpectPrepare("any").WillBeClosed()
|
||||||
@@ -303,6 +446,93 @@ func TestWithAcceptable(t *testing.T) {
|
|||||||
assert.True(t, conn.accept(acceptableErr3))
|
assert.True(t, conn.accept(acceptableErr3))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProvider(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
_ = connManager.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
primaryDSN := "primary:password@tcp(127.0.0.1:3306)/primary_db"
|
||||||
|
replicasDSN := []string{
|
||||||
|
"replica_one:pwd@tcp(localhost:3306)/replica_one",
|
||||||
|
"replica_two:pwd@tcp(localhost:3306)/replica_two",
|
||||||
|
"replica_three:pwd@tcp(localhost:3306)/replica_three",
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryDB, err := connManager.GetResource(primaryDSN, func() (io.Closer, error) { return sql.Open(mysqlDriverName, primaryDSN) })
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, primaryDB)
|
||||||
|
replicaOneDB, err := connManager.GetResource(replicasDSN[0], func() (io.Closer, error) { return sql.Open(mysqlDriverName, replicasDSN[0]) })
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, replicaOneDB)
|
||||||
|
replicaTwoDB, err := connManager.GetResource(replicasDSN[1], func() (io.Closer, error) { return sql.Open(mysqlDriverName, replicasDSN[1]) })
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, replicaTwoDB)
|
||||||
|
replicaThreeDB, err := connManager.GetResource(replicasDSN[2], func() (io.Closer, error) { return sql.Open(mysqlDriverName, replicasDSN[2]) })
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, replicaThreeDB)
|
||||||
|
|
||||||
|
sc := &commonSqlConn{}
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, policyRoundRobin, nil)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, primaryDB, db)
|
||||||
|
|
||||||
|
ctx = WithWrite(ctx)
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, primaryDB, db)
|
||||||
|
|
||||||
|
ctx = WithReadPrimary(ctx)
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, primaryDB, db)
|
||||||
|
|
||||||
|
// no mode set, should return primary
|
||||||
|
ctx = context.Background()
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, policyRoundRobin, replicasDSN)
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, primaryDB, db)
|
||||||
|
|
||||||
|
ctx = WithReadReplica(ctx)
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, policyRoundRobin, []string{replicasDSN[0]})
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, replicaOneDB, db)
|
||||||
|
|
||||||
|
// default policy is round-robin
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, policyRoundRobin, replicasDSN)
|
||||||
|
replicas := []io.Closer{replicaOneDB, replicaTwoDB, replicaThreeDB}
|
||||||
|
for i := 0; i < len(replicasDSN); i++ {
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, replicas[i], db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// random policy
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, policyRandom, replicasDSN)
|
||||||
|
for i := 0; i < len(replicasDSN); i++ {
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Contains(t, replicas, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown policy
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, "unknown", replicasDSN)
|
||||||
|
_, err = sc.connProv(ctx)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
// empty policy transforms to round-robin
|
||||||
|
sc.connProv = getConnProvider(sc, mysqlDriverName, primaryDSN, "", replicasDSN)
|
||||||
|
for i := 0; i < len(replicasDSN); i++ {
|
||||||
|
db, err = sc.connProv(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, replicas[i], db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildConn() (mock sqlmock.Sqlmock, err error) {
|
func buildConn() (mock sqlmock.Sqlmock, err error) {
|
||||||
_, err = connManager.GetResource(mockedDatasource, func() (io.Closer, error) {
|
_, err = connManager.GetResource(mockedDatasource, func() (io.Closer, error) {
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func getCachedSqlConn(driverName, server string) (*sql.DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if driverName != mysqlDriverName {
|
if driverName == mysqlDriverName {
|
||||||
if cfg, e := mysql.ParseDSN(server); e != nil {
|
if cfg, e := mysql.ParseDSN(server); e != nil {
|
||||||
// if cannot parse, don't collect the metrics
|
// if cannot parse, don't collect the metrics
|
||||||
logx.Error(e)
|
logx.Error(e)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ func begin(db *sql.DB) (trans, error) {
|
|||||||
|
|
||||||
func transact(ctx context.Context, db *commonSqlConn, b beginnable,
|
func transact(ctx context.Context, db *commonSqlConn, b beginnable,
|
||||||
fn func(context.Context, Session) error) (err error) {
|
fn func(context.Context, Session) error) (err error) {
|
||||||
conn, err := db.connProv()
|
conn, err := db.connProv(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.onError(ctx, err)
|
db.onError(ctx, err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func TestTxExceptions(t *testing.T) {
|
|||||||
|
|
||||||
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
||||||
conn := &commonSqlConn{
|
conn := &commonSqlConn{
|
||||||
connProv: func() (*sql.DB, error) {
|
connProv: func(ctx context.Context) (*sql.DB, error) {
|
||||||
return nil, errors.New("foo")
|
return nil, errors.New("foo")
|
||||||
},
|
},
|
||||||
beginTx: begin,
|
beginTx: begin,
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -17,7 +17,7 @@ require (
|
|||||||
github.com/jhump/protoreflect v1.17.0
|
github.com/jhump/protoreflect v1.17.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2
|
github.com/pelletier/go-toml/v2 v2.2.2
|
||||||
github.com/prometheus/client_golang v1.21.1
|
github.com/prometheus/client_golang v1.21.1
|
||||||
github.com/redis/go-redis/v9 v9.10.0
|
github.com/redis/go-redis/v9 v9.11.0
|
||||||
github.com/spaolacci/murmur3 v1.1.0
|
github.com/spaolacci/murmur3 v1.1.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15
|
go.etcd.io/etcd/api/v3 v3.5.15
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -158,8 +158,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type (
|
|||||||
CheckInterval time.Duration `json:",default=10s"`
|
CheckInterval time.Duration `json:",default=10s"`
|
||||||
// ProfilingDuration is the duration for which profiling data is collected.
|
// ProfilingDuration is the duration for which profiling data is collected.
|
||||||
ProfilingDuration time.Duration `json:",default=2m"`
|
ProfilingDuration time.Duration `json:",default=2m"`
|
||||||
// CpuThreshold the collection is allowed only when the current service cpu < CpuThreshold
|
// CpuThreshold the collection is allowed only when the current service cpu > CpuThreshold
|
||||||
CpuThreshold int64 `json:",default=700,range=[0:1000)"`
|
CpuThreshold int64 `json:",default=700,range=[0:1000)"`
|
||||||
|
|
||||||
// ProfileType is the type of profiling to be performed.
|
// ProfileType is the type of profiling to be performed.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
[English](readme.md) | 简体中文
|
[English](readme.md) | 简体中文
|
||||||
|
|
||||||
[](https://github.com/zeromicro/go-zero/actions)
|
|
||||||
[](https://goreportcard.com/report/github.com/zeromicro/go-zero)
|
[](https://goreportcard.com/report/github.com/zeromicro/go-zero)
|
||||||
[](https://goproxy.cn/stats/github.com/zeromicro/go-zero/badges/download-count.svg)
|
[](https://goproxy.cn/stats/github.com/zeromicro/go-zero/badges/download-count.svg)
|
||||||
[](https://codecov.io/gh/zeromicro/go-zero)
|
[](https://codecov.io/gh/zeromicro/go-zero)
|
||||||
@@ -304,6 +303,7 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
|
|||||||
>105. 昆仑万维科技股份有限公司
|
>105. 昆仑万维科技股份有限公司
|
||||||
>106. 无锡盛算信息技术有限公司
|
>106. 无锡盛算信息技术有限公司
|
||||||
>107. 深圳市聚货通信息科技有限公司
|
>107. 深圳市聚货通信息科技有限公司
|
||||||
|
>108. 浙江银盾云科技有限公司
|
||||||
|
|
||||||
如果贵公司也已使用 go-zero,欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。
|
如果贵公司也已使用 go-zero,欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ go-zero is a web and rpc framework with lots of builtin engineering practices. I
|
|||||||
|
|
||||||
<div align=center>
|
<div align=center>
|
||||||
|
|
||||||
[](https://github.com/zeromicro/go-zero/actions)
|
|
||||||
[](https://codecov.io/gh/zeromicro/go-zero)
|
[](https://codecov.io/gh/zeromicro/go-zero)
|
||||||
[](https://goreportcard.com/report/github.com/zeromicro/go-zero)
|
[](https://goreportcard.com/report/github.com/zeromicro/go-zero)
|
||||||
[](https://github.com/zeromicro/go-zero)
|
[](https://github.com/zeromicro/go-zero)
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ var ErrSignatureConfig = errors.New("bad config for Signature")
|
|||||||
type engine struct {
|
type engine struct {
|
||||||
conf RestConf
|
conf RestConf
|
||||||
routes []featuredRoutes
|
routes []featuredRoutes
|
||||||
// timeout is the max timeout of all routes
|
// timeout is the max timeout of all routes,
|
||||||
|
// and is used to set http.Server.ReadTimeout and http.Server.WriteTimeout.
|
||||||
|
// this network timeout is used to avoid DoS attacks by sending data slowly
|
||||||
|
// or receiving data slowly with many connections to exhaust server resources.
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
unauthorizedCallback handler.UnauthorizedCallback
|
unauthorizedCallback handler.UnauthorizedCallback
|
||||||
unsignedCallback handler.UnsignedCallback
|
unsignedCallback handler.UnsignedCallback
|
||||||
@@ -60,11 +63,7 @@ func (ng *engine) addRoutes(r featuredRoutes) {
|
|||||||
}
|
}
|
||||||
ng.routes = append(ng.routes, r)
|
ng.routes = append(ng.routes, r)
|
||||||
|
|
||||||
// need to guarantee the timeout is the max of all routes
|
ng.mightUpdateTimeout(r)
|
||||||
// otherwise impossible to set http.Server.ReadTimeout & WriteTimeout
|
|
||||||
if r.timeout > ng.timeout {
|
|
||||||
ng.timeout = r.timeout
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSSERoutes(routes []Route) []Route {
|
func buildSSERoutes(routes []Route) []Route {
|
||||||
@@ -192,11 +191,12 @@ func (ng *engine) checkedMaxBytes(bytes int64) int64 {
|
|||||||
return ng.conf.MaxBytes
|
return ng.conf.MaxBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ng *engine) checkedTimeout(timeout time.Duration) time.Duration {
|
func (ng *engine) checkedTimeout(timeout *time.Duration) time.Duration {
|
||||||
if timeout > 0 {
|
if timeout != nil {
|
||||||
return timeout
|
return *timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if timeout not set in featured routes, use global timeout
|
||||||
return time.Duration(ng.conf.Timeout) * time.Millisecond
|
return time.Duration(ng.conf.Timeout) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +232,28 @@ func (ng *engine) hasTimeout() bool {
|
|||||||
return ng.conf.Middlewares.Timeout && ng.timeout > 0
|
return ng.conf.Middlewares.Timeout && ng.timeout > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mightUpdateTimeout checks if the route timeout is greater than the current,
|
||||||
|
// and updates the engine's timeout accordingly.
|
||||||
|
func (ng *engine) mightUpdateTimeout(r featuredRoutes) {
|
||||||
|
// if global timeout is set to 0, it means no need to set read/write timeout
|
||||||
|
// if route timeout is nil, no need to update ng.timeout
|
||||||
|
if ng.timeout == 0 || r.timeout == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if route timeout is 0 (means no timeout), cannot set read/write timeout
|
||||||
|
if *r.timeout == 0 {
|
||||||
|
ng.timeout = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to guarantee the timeout is the max of all routes
|
||||||
|
// otherwise impossible to set http.Server.ReadTimeout & WriteTimeout
|
||||||
|
if *r.timeout > ng.timeout {
|
||||||
|
ng.timeout = *r.timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// notFoundHandler returns a middleware that handles 404 not found requests.
|
// notFoundHandler returns a middleware that handles 404 not found requests.
|
||||||
func (ng *engine) notFoundHandler(next http.Handler) http.Handler {
|
func (ng *engine) notFoundHandler(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -333,7 +355,7 @@ func (ng *engine) start(router httpx.Router, opts ...StartOption) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure user defined options overwrite default options
|
// make sure user defined options overwrite default options
|
||||||
opts = append([]StartOption{ng.withTimeout()}, opts...)
|
opts = append([]StartOption{ng.withNetworkTimeout()}, opts...)
|
||||||
|
|
||||||
if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
|
if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
|
||||||
return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, opts...)
|
return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, opts...)
|
||||||
@@ -356,7 +378,7 @@ func (ng *engine) use(middleware Middleware) {
|
|||||||
ng.middlewares = append(ng.middlewares, middleware)
|
ng.middlewares = append(ng.middlewares, middleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ng *engine) withTimeout() internal.StartOption {
|
func (ng *engine) withNetworkTimeout() internal.StartOption {
|
||||||
return func(svr *http.Server) {
|
return func(svr *http.Server) {
|
||||||
if !ng.hasTimeout() {
|
if !ng.hasTimeout() {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -73,7 +73,17 @@ Verbose: true
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Handler: func(w http.ResponseWriter, r *http.Request) {},
|
Handler: func(w http.ResponseWriter, r *http.Request) {},
|
||||||
}},
|
}},
|
||||||
timeout: time.Minute,
|
timeout: ptrOfDuration(time.Minute),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jwt: jwtSetting{},
|
||||||
|
signature: signatureSetting{},
|
||||||
|
routes: []Route{{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {},
|
||||||
|
}},
|
||||||
|
timeout: ptrOfDuration(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
priority: true,
|
priority: true,
|
||||||
@@ -84,7 +94,7 @@ Verbose: true
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Handler: func(w http.ResponseWriter, r *http.Request) {},
|
Handler: func(w http.ResponseWriter, r *http.Request) {},
|
||||||
}},
|
}},
|
||||||
timeout: time.Second,
|
timeout: ptrOfDuration(time.Second),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
priority: true,
|
priority: true,
|
||||||
@@ -227,8 +237,12 @@ Verbose: true
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
timeout := time.Second * 3
|
timeout := time.Second * 3
|
||||||
if route.timeout > timeout {
|
if route.timeout != nil {
|
||||||
timeout = route.timeout
|
if *route.timeout == 0 {
|
||||||
|
timeout = 0
|
||||||
|
} else if *route.timeout > timeout {
|
||||||
|
timeout = *route.timeout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(t, timeout, ng.timeout)
|
assert.Equal(t, timeout, ng.timeout)
|
||||||
})
|
})
|
||||||
@@ -236,10 +250,69 @@ Verbose: true
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewEngine_unsignedCallback(t *testing.T) {
|
||||||
|
priKeyfile, err := fs.TempFilenameWithText(priKey)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(priKeyfile)
|
||||||
|
|
||||||
|
yaml := `Name: foo
|
||||||
|
Host: localhost
|
||||||
|
Port: 0
|
||||||
|
Middlewares:
|
||||||
|
Log: false
|
||||||
|
`
|
||||||
|
route := featuredRoutes{
|
||||||
|
priority: true,
|
||||||
|
jwt: jwtSetting{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
signature: signatureSetting{
|
||||||
|
enabled: true,
|
||||||
|
SignatureConf: SignatureConf{
|
||||||
|
Strict: true,
|
||||||
|
PrivateKeys: []PrivateKeyConf{
|
||||||
|
{
|
||||||
|
Fingerprint: "a",
|
||||||
|
KeyFile: priKeyfile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: []Route{{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var index int32
|
||||||
|
t.Run(fmt.Sprintf("%s-%v", yaml, route.routes), func(t *testing.T) {
|
||||||
|
var cnf RestConf
|
||||||
|
assert.Nil(t, conf.LoadFromYamlBytes([]byte(yaml), &cnf))
|
||||||
|
ng := newEngine(cnf)
|
||||||
|
if atomic.AddInt32(&index, 1)%2 == 0 {
|
||||||
|
ng.setUnsignedCallback(func(w http.ResponseWriter, r *http.Request,
|
||||||
|
next http.Handler, strict bool, code int) {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ng.addRoutes(route)
|
||||||
|
ng.use(func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NotNil(t, ng.start(mockedRouter{}, func(svr *http.Server) {
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert.Equal(t, time.Duration(time.Second*3), ng.timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestEngine_checkedTimeout(t *testing.T) {
|
func TestEngine_checkedTimeout(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
timeout time.Duration
|
timeout *time.Duration
|
||||||
expect time.Duration
|
expect time.Duration
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -248,17 +321,17 @@ func TestEngine_checkedTimeout(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "less",
|
name: "less",
|
||||||
timeout: time.Millisecond * 500,
|
timeout: ptrOfDuration(time.Millisecond * 500),
|
||||||
expect: time.Millisecond * 500,
|
expect: time.Millisecond * 500,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "equal",
|
name: "equal",
|
||||||
timeout: time.Second,
|
timeout: ptrOfDuration(time.Second),
|
||||||
expect: time.Second,
|
expect: time.Second,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "more",
|
name: "more",
|
||||||
timeout: time.Millisecond * 1500,
|
timeout: ptrOfDuration(time.Millisecond * 1500),
|
||||||
expect: time.Millisecond * 1500,
|
expect: time.Millisecond * 1500,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -401,7 +474,7 @@ func TestEngine_withTimeout(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
svr := &http.Server{}
|
svr := &http.Server{}
|
||||||
ng.withTimeout()(svr)
|
ng.withNetworkTimeout()(svr)
|
||||||
|
|
||||||
assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*4/5, svr.ReadTimeout)
|
assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*4/5, svr.ReadTimeout)
|
||||||
assert.Equal(t, time.Duration(0), svr.ReadHeaderTimeout)
|
assert.Equal(t, time.Duration(0), svr.ReadHeaderTimeout)
|
||||||
@@ -451,7 +524,7 @@ func TestEngine_ReadWriteTimeout(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
svr := &http.Server{}
|
svr := &http.Server{}
|
||||||
ng.withTimeout()(svr)
|
ng.withNetworkTimeout()(svr)
|
||||||
|
|
||||||
assert.Equal(t, time.Duration(0), svr.ReadHeaderTimeout)
|
assert.Equal(t, time.Duration(0), svr.ReadHeaderTimeout)
|
||||||
assert.Equal(t, time.Duration(0), svr.IdleTimeout)
|
assert.Equal(t, time.Duration(0), svr.IdleTimeout)
|
||||||
|
|||||||
@@ -119,6 +119,16 @@ func (s *Server) Use(middleware Middleware) {
|
|||||||
s.ngin.use(middleware)
|
s.ngin.use(middleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build builds the Server and binds the routes to the router.
|
||||||
|
func (s *Server) build() error {
|
||||||
|
return s.ngin.bindRoutes(s.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve serves the HTTP requests using the Server's router.
|
||||||
|
func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.router.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
// ToMiddleware converts the given handler to a Middleware.
|
// ToMiddleware converts the given handler to a Middleware.
|
||||||
func ToMiddleware(handler func(next http.Handler) http.Handler) Middleware {
|
func ToMiddleware(handler func(next http.Handler) http.Handler) Middleware {
|
||||||
return func(handle http.HandlerFunc) http.HandlerFunc {
|
return func(handle http.HandlerFunc) http.HandlerFunc {
|
||||||
@@ -283,14 +293,14 @@ func WithSignature(signature SignatureConf) RouteOption {
|
|||||||
func WithSSE() RouteOption {
|
func WithSSE() RouteOption {
|
||||||
return func(r *featuredRoutes) {
|
return func(r *featuredRoutes) {
|
||||||
r.sse = true
|
r.sse = true
|
||||||
r.timeout = 0
|
r.timeout = ptrOfDuration(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTimeout returns a RouteOption to set timeout with given value.
|
// WithTimeout returns a RouteOption to set timeout with given value.
|
||||||
func WithTimeout(timeout time.Duration) RouteOption {
|
func WithTimeout(timeout time.Duration) RouteOption {
|
||||||
return func(r *featuredRoutes) {
|
return func(r *featuredRoutes) {
|
||||||
r.timeout = timeout
|
r.timeout = &timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +335,10 @@ func handleError(err error) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ptrOfDuration(d time.Duration) *time.Duration {
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
func validateSecret(secret string) {
|
func validateSecret(secret string) {
|
||||||
if len(secret) < 8 {
|
if len(secret) < 8 {
|
||||||
panic("secret's length can't be less than 8")
|
panic("secret's length can't be less than 8")
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ func TestWithPriority(t *testing.T) {
|
|||||||
func TestWithTimeout(t *testing.T) {
|
func TestWithTimeout(t *testing.T) {
|
||||||
var fr featuredRoutes
|
var fr featuredRoutes
|
||||||
WithTimeout(time.Hour)(&fr)
|
WithTimeout(time.Hour)(&fr)
|
||||||
assert.Equal(t, time.Hour, fr.timeout)
|
assert.Equal(t, time.Hour, *fr.timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithTLSConfig(t *testing.T) {
|
func TestWithTLSConfig(t *testing.T) {
|
||||||
@@ -819,6 +819,6 @@ func TestServerEmbedFileSystem(t *testing.T) {
|
|||||||
// serve(server, w, r)
|
// serve(server, w, r)
|
||||||
// // verify the response
|
// // verify the response
|
||||||
func serve(s *Server, w http.ResponseWriter, r *http.Request) {
|
func serve(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||||
s.ngin.bindRoutes(s.router)
|
_ = s.build()
|
||||||
s.router.ServeHTTP(w, r)
|
s.serve(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
27
rest/serverless.go
Normal file
27
rest/serverless.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Serverless is a wrapper around Server that allows it to be used in serverless environments.
|
||||||
|
type Serverless struct {
|
||||||
|
server *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServerless creates a new Serverless instance from the provided Server.
|
||||||
|
func NewServerless(server *Server) (*Serverless, error) {
|
||||||
|
// Ensure the server is built before using it in a serverless context.
|
||||||
|
// Why not call server.build() when serving requests,
|
||||||
|
// is because we need to ensure fail fast behavior.
|
||||||
|
if err := server.build(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Serverless{
|
||||||
|
server: server,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve handles HTTP requests by delegating them to the underlying Server instance.
|
||||||
|
func (s *Serverless) Serve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.server.serve(w, r)
|
||||||
|
}
|
||||||
67
rest/serverless_test.go
Normal file
67
rest/serverless_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx/logtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewServerless(t *testing.T) {
|
||||||
|
logtest.Discard(t)
|
||||||
|
|
||||||
|
const configYaml = `
|
||||||
|
Name: foo
|
||||||
|
Host: localhost
|
||||||
|
Port: 0
|
||||||
|
`
|
||||||
|
var cnf RestConf
|
||||||
|
assert.Nil(t, conf.LoadFromYamlBytes([]byte(configYaml), &cnf))
|
||||||
|
|
||||||
|
svr, err := NewServer(cnf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
svr.AddRoute(Route{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("Hello World"))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
serverless, err := NewServerless(svr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
serverless.Serve(w, r)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Equal(t, "Hello World", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewServerlessWithError(t *testing.T) {
|
||||||
|
logtest.Discard(t)
|
||||||
|
|
||||||
|
const configYaml = `
|
||||||
|
Name: foo
|
||||||
|
Host: localhost
|
||||||
|
Port: 0
|
||||||
|
`
|
||||||
|
var cnf RestConf
|
||||||
|
assert.Nil(t, conf.LoadFromYamlBytes([]byte(configYaml), &cnf))
|
||||||
|
|
||||||
|
svr, err := NewServer(cnf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
svr.AddRoute(Route{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "notstartwith/",
|
||||||
|
Handler: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err = NewServerless(svr)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
featuredRoutes struct {
|
featuredRoutes struct {
|
||||||
timeout time.Duration
|
timeout *time.Duration
|
||||||
priority bool
|
priority bool
|
||||||
jwt jwtSetting
|
jwt jwtSetting
|
||||||
signature signatureSetting
|
signature signatureSetting
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ func Parse(tag string) (*Tags, error) {
|
|||||||
|
|
||||||
// Get gets tag value by specified key
|
// Get gets tag value by specified key
|
||||||
func (t *Tags) Get(key string) (*Tag, error) {
|
func (t *Tags) Get(key string) (*Tag, error) {
|
||||||
|
if t == nil {
|
||||||
|
return nil, errTagNotExist
|
||||||
|
}
|
||||||
for _, tag := range t.tags {
|
for _, tag := range t.tags {
|
||||||
if tag.Key == key {
|
if tag.Key == key {
|
||||||
return tag, nil
|
return tag, nil
|
||||||
@@ -60,6 +63,9 @@ func (t *Tags) Get(key string) (*Tag, error) {
|
|||||||
|
|
||||||
// Keys returns all keys in Tags
|
// Keys returns all keys in Tags
|
||||||
func (t *Tags) Keys() []string {
|
func (t *Tags) Keys() []string {
|
||||||
|
if t == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
var keys []string
|
var keys []string
|
||||||
for _, tag := range t.tags {
|
for _, tag := range t.tags {
|
||||||
keys = append(keys, tag.Key)
|
keys = append(keys, tag.Key)
|
||||||
@@ -69,5 +75,8 @@ func (t *Tags) Keys() []string {
|
|||||||
|
|
||||||
// Tags returns all tags in Tags
|
// Tags returns all tags in Tags
|
||||||
func (t *Tags) Tags() []*Tag {
|
func (t *Tags) Tags() []*Tag {
|
||||||
|
if t == nil {
|
||||||
|
return []*Tag{}
|
||||||
|
}
|
||||||
return t.tags
|
return t.tags
|
||||||
}
|
}
|
||||||
|
|||||||
68
tools/goctl/api/spec/tags_test.go
Normal file
68
tools/goctl/api/spec/tags_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package spec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTags_Get(t *testing.T) {
|
||||||
|
tags := &Tags{
|
||||||
|
tags: []*Tag{
|
||||||
|
{Key: "json", Name: "foo", Options: []string{"omitempty"}},
|
||||||
|
{Key: "xml", Name: "bar", Options: nil},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := tags.Get("json")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, tag)
|
||||||
|
assert.Equal(t, "json", tag.Key)
|
||||||
|
assert.Equal(t, "foo", tag.Name)
|
||||||
|
|
||||||
|
_, err = tags.Get("yaml")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
var nilTags *Tags
|
||||||
|
_, err = nilTags.Get("json")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTags_Keys(t *testing.T) {
|
||||||
|
tags := &Tags{
|
||||||
|
tags: []*Tag{
|
||||||
|
{Key: "json", Name: "foo", Options: []string{"omitempty"}},
|
||||||
|
{Key: "xml", Name: "bar", Options: nil},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := tags.Keys()
|
||||||
|
expected := []string{"json", "xml"}
|
||||||
|
assert.Equal(t, expected, keys)
|
||||||
|
|
||||||
|
var nilTags *Tags
|
||||||
|
nilKeys := nilTags.Keys()
|
||||||
|
assert.Empty(t, nilKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTags_Tags(t *testing.T) {
|
||||||
|
tags := &Tags{
|
||||||
|
tags: []*Tag{
|
||||||
|
{Key: "json", Name: "foo", Options: []string{"omitempty"}},
|
||||||
|
{Key: "xml", Name: "bar", Options: nil},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tags.Tags()
|
||||||
|
assert.Len(t, result, 2)
|
||||||
|
assert.Equal(t, "json", result[0].Key)
|
||||||
|
assert.Equal(t, "foo", result[0].Name)
|
||||||
|
assert.Equal(t, []string{"omitempty"}, result[0].Options)
|
||||||
|
assert.Equal(t, "xml", result[1].Key)
|
||||||
|
assert.Equal(t, "bar", result[1].Name)
|
||||||
|
assert.Nil(t, result[1].Options)
|
||||||
|
|
||||||
|
var nilTags *Tags
|
||||||
|
nilResult := nilTags.Tags()
|
||||||
|
assert.Empty(t, nilResult)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/emicklei/proto v1.14.1
|
github.com/emicklei/proto v1.14.2
|
||||||
github.com/fatih/structtag v1.2.0
|
github.com/fatih/structtag v1.2.0
|
||||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e
|
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e
|
||||||
github.com/go-sql-driver/mysql v1.9.0
|
github.com/go-sql-driver/mysql v1.9.0
|
||||||
@@ -16,7 +16,7 @@ require (
|
|||||||
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
|
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
|
||||||
github.com/zeromicro/antlr v0.0.1
|
github.com/zeromicro/antlr v0.0.1
|
||||||
github.com/zeromicro/ddl-parser v1.0.5
|
github.com/zeromicro/ddl-parser v1.0.5
|
||||||
github.com/zeromicro/go-zero v1.8.3
|
github.com/zeromicro/go-zero v1.8.4
|
||||||
golang.org/x/text v0.22.0
|
golang.org/x/text v0.22.0
|
||||||
google.golang.org/grpc v1.65.0
|
google.golang.org/grpc v1.65.0
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
@@ -25,8 +25,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
|
github.com/alicebob/miniredis/v2 v2.35.0 // indirect
|
||||||
github.com/alicebob/miniredis/v2 v2.34.0 // indirect
|
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec // indirect
|
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
@@ -49,6 +48,8 @@ require (
|
|||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grafana/pyroscope-go v1.2.2 // indirect
|
||||||
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
@@ -72,7 +73,7 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.8.0 // indirect
|
github.com/redis/go-redis/v9 v9.10.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
|
|
||||||
github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
|
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec h1:EEyRvzmpEUZ+I8WmD5cw/vY8EqhambkOqy5iFr0908A=
|
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec h1:EEyRvzmpEUZ+I8WmD5cw/vY8EqhambkOqy5iFr0908A=
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
@@ -32,8 +30,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
github.com/emicklei/proto v1.14.1 h1:fFq+Bj70XXZWXWikcVRvYZxrMS4KIIiPAqdJ8vPrenY=
|
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
|
||||||
github.com/emicklei/proto v1.14.1/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||||
@@ -77,6 +75,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
|
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
|
||||||
|
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
|
||||||
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||||
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
@@ -146,8 +148,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -183,8 +185,8 @@ github.com/zeromicro/antlr v0.0.1 h1:CQpIn/dc0pUjgGQ81y98s/NGOm2Hfru2NNio2I9mQgk
|
|||||||
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
|
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
|
||||||
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
|
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
|
||||||
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
|
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
|
||||||
github.com/zeromicro/go-zero v1.8.3 h1:AwpBJQLAsZAt4OOnK0eR8UU1Ja2RFBIXfKkHdnXQKfc=
|
github.com/zeromicro/go-zero v1.8.4 h1:3s7kOoThCnkDoqCafsqSX58Y9osYTBIa5QEmomw07TE=
|
||||||
github.com/zeromicro/go-zero v1.8.3/go.mod h1:EnuEA3XdIQvAvc4WWTskRTO0jM2/aQi7OXv1gKWRNJ0=
|
github.com/zeromicro/go-zero v1.8.4/go.mod h1:eM5f6If/RF+jG1wSCmlvfXD2h2l23vJwETI8oDpjYt4=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
||||||
|
|||||||
Reference in New Issue
Block a user