mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-31 18:45:29 +08:00
Compare commits
31 Commits
tools/goct
...
v1.8.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f2b589d4d | ||
|
|
19fec36d24 | ||
|
|
f037bf344d | ||
|
|
d99cf35b07 | ||
|
|
f459f1b5ff | ||
|
|
0140fd417b | ||
|
|
7969e0ca38 | ||
|
|
91c885b5b0 | ||
|
|
d4cccca387 | ||
|
|
4b2095ed03 | ||
|
|
1229eeb2d2 | ||
|
|
9142b146c5 | ||
|
|
8a1b2d5aed | ||
|
|
da5d39e6ca | ||
|
|
68c5a17c67 | ||
|
|
b53f9f5f2d | ||
|
|
36d57626b6 | ||
|
|
4e36ba832f | ||
|
|
a44954a771 | ||
|
|
f3edd4b880 | ||
|
|
2de3e397ff | ||
|
|
a435eb56f2 | ||
|
|
d80761c147 | ||
|
|
e7bd0d8b60 | ||
|
|
b109b3ef4c | ||
|
|
e3c371ac89 | ||
|
|
15eb6f4f6d | ||
|
|
4d3681b71c | ||
|
|
a682bda0bb | ||
|
|
45b27ad93a | ||
|
|
292a8302a1 |
@@ -8,16 +8,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/mathx"
|
|
||||||
"github.com/zeromicro/go-zero/core/proc"
|
"github.com/zeromicro/go-zero/core/proc"
|
||||||
"github.com/zeromicro/go-zero/core/stat"
|
"github.com/zeromicro/go-zero/core/stat"
|
||||||
"github.com/zeromicro/go-zero/core/stringx"
|
"github.com/zeromicro/go-zero/core/stringx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const numHistoryReasons = 5
|
||||||
numHistoryReasons = 5
|
|
||||||
timeFormat = "15:04:05"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrServiceUnavailable is returned when the Breaker state is open.
|
// ErrServiceUnavailable is returned when the Breaker state is open.
|
||||||
var ErrServiceUnavailable = errors.New("circuit breaker is open")
|
var ErrServiceUnavailable = errors.New("circuit breaker is open")
|
||||||
@@ -262,9 +258,9 @@ type errorWindow struct {
|
|||||||
|
|
||||||
func (ew *errorWindow) add(reason string) {
|
func (ew *errorWindow) add(reason string) {
|
||||||
ew.lock.Lock()
|
ew.lock.Lock()
|
||||||
ew.reasons[ew.index] = fmt.Sprintf("%s %s", time.Now().Format(timeFormat), reason)
|
ew.reasons[ew.index] = fmt.Sprintf("%s %s", time.Now().Format(time.TimeOnly), reason)
|
||||||
ew.index = (ew.index + 1) % numHistoryReasons
|
ew.index = (ew.index + 1) % numHistoryReasons
|
||||||
ew.count = mathx.MinInt(ew.count+1, numHistoryReasons)
|
ew.count = min(ew.count+1, numHistoryReasons)
|
||||||
ew.lock.Unlock()
|
ew.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,21 +86,16 @@ func TestConsistentHashIncrementalTransfer(t *testing.T) {
|
|||||||
|
|
||||||
func TestConsistentHashTransferOnFailure(t *testing.T) {
|
func TestConsistentHashTransferOnFailure(t *testing.T) {
|
||||||
index := 41
|
index := 41
|
||||||
keys, newKeys := getKeysBeforeAndAfterFailure(t, "localhost:", index)
|
ratioNotExists := getTransferRatioOnFailure(t, index)
|
||||||
var transferred int
|
assert.True(t, ratioNotExists == 0, fmt.Sprintf("%d: %f", index, ratioNotExists))
|
||||||
for k, v := range newKeys {
|
index = 13
|
||||||
if v != keys[k] {
|
ratio := getTransferRatioOnFailure(t, index)
|
||||||
transferred++
|
assert.True(t, ratio < 2.5/keySize, fmt.Sprintf("%d: %f", index, ratio))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ratio := float32(transferred) / float32(requestSize)
|
|
||||||
assert.True(t, ratio < 2.5/float32(keySize), fmt.Sprintf("%d: %f", index, ratio))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConsistentHashLeastTransferOnFailure(t *testing.T) {
|
func TestConsistentHashLeastTransferOnFailure(t *testing.T) {
|
||||||
prefix := "localhost:"
|
prefix := "localhost:"
|
||||||
index := 41
|
index := 13
|
||||||
keys, newKeys := getKeysBeforeAndAfterFailure(t, prefix, index)
|
keys, newKeys := getKeysBeforeAndAfterFailure(t, prefix, index)
|
||||||
for k, v := range keys {
|
for k, v := range keys {
|
||||||
newV := newKeys[k]
|
newV := newKeys[k]
|
||||||
@@ -164,6 +159,17 @@ func getKeysBeforeAndAfterFailure(t *testing.T, prefix string, index int) (map[i
|
|||||||
return keys, newKeys
|
return keys, newKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTransferRatioOnFailure(t *testing.T, index int) float32 {
|
||||||
|
keys, newKeys := getKeysBeforeAndAfterFailure(t, "localhost:", index)
|
||||||
|
var transferred int
|
||||||
|
for k, v := range newKeys {
|
||||||
|
if v != keys[k] {
|
||||||
|
transferred++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return float32(transferred) / float32(requestSize)
|
||||||
|
}
|
||||||
|
|
||||||
type mockNode struct {
|
type mockNode struct {
|
||||||
addr string
|
addr string
|
||||||
id int
|
id int
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package hash
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"encoding/hex"
|
||||||
|
|
||||||
"github.com/spaolacci/murmur3"
|
"github.com/spaolacci/murmur3"
|
||||||
)
|
)
|
||||||
@@ -20,6 +20,7 @@ func Md5(data []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Md5Hex returns the md5 hex string of data.
|
// Md5Hex returns the md5 hex string of data.
|
||||||
|
// This function is optimized for better performance than fmt.Sprintf.
|
||||||
func Md5Hex(data []byte) string {
|
func Md5Hex(data []byte) string {
|
||||||
return fmt.Sprintf("%x", Md5(data))
|
return hex.EncodeToString(Md5(data))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dateFormat = "2006-01-02"
|
|
||||||
hoursPerDay = 24
|
hoursPerDay = 24
|
||||||
bufferSize = 100
|
bufferSize = 100
|
||||||
defaultDirMode = 0o755
|
defaultDirMode = 0o755
|
||||||
@@ -116,7 +115,7 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat)
|
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(time.DateOnly)
|
||||||
buf.WriteString(r.filename)
|
buf.WriteString(r.filename)
|
||||||
buf.WriteString(r.delimiter)
|
buf.WriteString(r.delimiter)
|
||||||
buf.WriteString(boundary)
|
buf.WriteString(boundary)
|
||||||
@@ -425,7 +424,7 @@ func compressLogFile(file string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getNowDate() string {
|
func getNowDate() string {
|
||||||
return time.Now().Format(dateFormat)
|
return time.Now().Format(time.DateOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNowDateInRFC3339Format() string {
|
func getNowDateInRFC3339Format() string {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("temp files", func(t *testing.T) {
|
t.Run("temp files", func(t *testing.T) {
|
||||||
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
|
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
|
||||||
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_ = f1.Close()
|
_ = f1.Close()
|
||||||
@@ -73,7 +73,7 @@ func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
|
|||||||
|
|
||||||
func TestDailyRotateRuleShallRotate(t *testing.T) {
|
func TestDailyRotateRuleShallRotate(t *testing.T) {
|
||||||
var rule DailyRotateRule
|
var rule DailyRotateRule
|
||||||
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(dateFormat)
|
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(time.DateOnly)
|
||||||
assert.True(t, rule.ShallRotate(0))
|
assert.True(t, rule.ShallRotate(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,12 +117,12 @@ func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("temp files", func(t *testing.T) {
|
t.Run("temp files", func(t *testing.T) {
|
||||||
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
|
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
|
||||||
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
|
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
|
||||||
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
|
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -144,12 +144,12 @@ func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no backups", func(t *testing.T) {
|
t.Run("no backups", func(t *testing.T) {
|
||||||
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
|
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
|
||||||
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
|
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
|
||||||
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
|
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -319,7 +319,7 @@ func TestRotateLoggerWrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// the following write calls cannot be changed to Write, because of DATA RACE.
|
// the following write calls cannot be changed to Write, because of DATA RACE.
|
||||||
logger.write([]byte(`foo`))
|
logger.write([]byte(`foo`))
|
||||||
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat)
|
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(time.DateOnly)
|
||||||
logger.write([]byte(`bar`))
|
logger.write([]byte(`bar`))
|
||||||
logger.Close()
|
logger.Close()
|
||||||
logger.write([]byte(`baz`))
|
logger.write([]byte(`baz`))
|
||||||
@@ -447,7 +447,7 @@ func TestRotateLoggerWithSizeLimitRotateRuleWrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// the following write calls cannot be changed to Write, because of DATA RACE.
|
// the following write calls cannot be changed to Write, because of DATA RACE.
|
||||||
logger.write([]byte(`foo`))
|
logger.write([]byte(`foo`))
|
||||||
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat)
|
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(time.DateOnly)
|
||||||
logger.write([]byte(`bar`))
|
logger.write([]byte(`bar`))
|
||||||
logger.Close()
|
logger.Close()
|
||||||
logger.write([]byte(`baz`))
|
logger.write([]byte(`baz`))
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
package mathx
|
package mathx
|
||||||
|
|
||||||
// MaxInt returns the larger one of a and b.
|
// MaxInt returns the larger one of a and b.
|
||||||
|
// Deprecated: use builtin max instead.
|
||||||
func MaxInt(a, b int) int {
|
func MaxInt(a, b int) int {
|
||||||
if a > b {
|
return max(a, b)
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MinInt returns the smaller one of a and b.
|
// MinInt returns the smaller one of a and b.
|
||||||
|
// Deprecated: use builtin min instead.
|
||||||
func MinInt(a, b int) int {
|
func MinInt(a, b int) int {
|
||||||
if a < b {
|
return min(a, b)
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/zeromicro/go-zero/core/stat"
|
"github.com/zeromicro/go-zero/core/stat"
|
||||||
"github.com/zeromicro/go-zero/core/trace"
|
"github.com/zeromicro/go-zero/core/trace"
|
||||||
"github.com/zeromicro/go-zero/internal/devserver"
|
"github.com/zeromicro/go-zero/internal/devserver"
|
||||||
|
"github.com/zeromicro/go-zero/internal/profiling"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -38,6 +39,8 @@ type (
|
|||||||
Telemetry trace.Config `json:",optional"`
|
Telemetry trace.Config `json:",optional"`
|
||||||
DevServer DevServerConfig `json:",optional"`
|
DevServer DevServerConfig `json:",optional"`
|
||||||
Shutdown proc.ShutdownConf `json:",optional"`
|
Shutdown proc.ShutdownConf `json:",optional"`
|
||||||
|
// Profiling is the configuration for continuous profiling.
|
||||||
|
Profiling profiling.Config `json:",optional"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,7 +73,9 @@ func (sc ServiceConf) SetUp() error {
|
|||||||
if len(sc.MetricsUrl) > 0 {
|
if len(sc.MetricsUrl) > 0 {
|
||||||
stat.SetReportWriter(stat.NewRemoteWriter(sc.MetricsUrl))
|
stat.SetReportWriter(stat.NewRemoteWriter(sc.MetricsUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
devserver.StartAgent(sc.DevServer)
|
devserver.StartAgent(sc.DevServer)
|
||||||
|
profiling.Start(sc.Profiling)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
clusterNameKey = "CLUSTER_NAME"
|
clusterNameKey = "CLUSTER_NAME"
|
||||||
testEnv = "test.v"
|
testEnv = "test.v"
|
||||||
timeFormat = "2006-01-02 15:04:05"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -45,7 +44,7 @@ func Report(msg string) {
|
|||||||
if fn != nil {
|
if fn != nil {
|
||||||
reported := lessExecutor.DoOrDiscard(func() {
|
reported := lessExecutor.DoOrDiscard(func() {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString(fmt.Sprintln(time.Now().Format(timeFormat)))
|
builder.WriteString(fmt.Sprintln(time.Now().Format(time.DateTime)))
|
||||||
if len(clusterName) > 0 {
|
if len(clusterName) > 0 {
|
||||||
builder.WriteString(fmt.Sprintf("cluster: %s\n", clusterName))
|
builder.WriteString(fmt.Sprintf("cluster: %s\n", clusterName))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/mathx"
|
|
||||||
"github.com/zeromicro/go-zero/core/stringx"
|
"github.com/zeromicro/go-zero/core/stringx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ func compare(v1, v2 string) int {
|
|||||||
fields1, fields2 := strings.Split(v1, "."), strings.Split(v2, ".")
|
fields1, fields2 := strings.Split(v1, "."), strings.Split(v2, ".")
|
||||||
ver1, ver2 := strsToInts(fields1), strsToInts(fields2)
|
ver1, ver2 := strsToInts(fields1), strsToInts(fields2)
|
||||||
ver1len, ver2len := len(ver1), len(ver2)
|
ver1len, ver2len := len(ver1), len(ver2)
|
||||||
shorter := mathx.MinInt(ver1len, ver2len)
|
shorter := min(ver1len, ver2len)
|
||||||
|
|
||||||
for i := 0; i < shorter; i++ {
|
for i := 0; i < shorter; i++ {
|
||||||
if ver1[i] == ver2[i] {
|
if ver1[i] == ver2[i] {
|
||||||
@@ -50,14 +50,7 @@ func compare(v1, v2 string) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return cmp.Compare(ver1len, ver2len)
|
||||||
if ver1len < ver2len {
|
|
||||||
return -1
|
|
||||||
} else if ver1len == ver2len {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func strsToInts(strs []string) []int64 {
|
func strsToInts(strs []string) []int64 {
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -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/alicebob/miniredis/v2 v2.34.0
|
github.com/alicebob/miniredis/v2 v2.35.0
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/fullstorydev/grpcurl v1.9.3
|
github.com/fullstorydev/grpcurl v1.9.3
|
||||||
github.com/go-sql-driver/mysql v1.9.0
|
github.com/go-sql-driver/mysql v1.9.0
|
||||||
@@ -12,16 +12,17 @@ require (
|
|||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/golang/protobuf v1.5.4
|
github.com/golang/protobuf v1.5.4
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/grafana/pyroscope-go v1.2.2
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.7.4
|
||||||
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.8.0
|
github.com/redis/go-redis/v9 v9.10.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
|
||||||
go.etcd.io/etcd/client/v3 v3.5.15
|
go.etcd.io/etcd/client/v3 v3.5.15
|
||||||
go.mongodb.org/mongo-driver v1.17.3
|
go.mongodb.org/mongo-driver v1.17.4
|
||||||
go.opentelemetry.io/otel v1.24.0
|
go.opentelemetry.io/otel v1.24.0
|
||||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
|
||||||
@@ -49,7 +50,6 @@ 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/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bufbuild/protocompile v0.14.1 // indirect
|
github.com/bufbuild/protocompile v0.14.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
@@ -72,6 +72,7 @@ require (
|
|||||||
github.com/google/gnostic-models v0.6.8 // indirect
|
github.com/google/gnostic-models v0.6.8 // indirect
|
||||||
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/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/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
|||||||
18
go.sum
18
go.sum
@@ -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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -82,6 +80,10 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
|
|||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/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=
|
||||||
@@ -156,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.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/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=
|
||||||
@@ -200,8 +202,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5
|
|||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
|
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
|
||||||
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
|
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
|
||||||
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
|
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
|
||||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||||
|
|||||||
263
internal/profiling/profiling.go
Normal file
263
internal/profiling/profiling.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package profiling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/pyroscope-go"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
"github.com/zeromicro/go-zero/core/proc"
|
||||||
|
"github.com/zeromicro/go-zero/core/stat"
|
||||||
|
"github.com/zeromicro/go-zero/core/threading"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultCheckInterval = time.Second * 10
|
||||||
|
defaultProfilingDuration = time.Minute * 2
|
||||||
|
defaultUploadRate = time.Second * 15
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Config struct {
|
||||||
|
// Name is the name of the application.
|
||||||
|
Name string `json:",optional,inherit"`
|
||||||
|
// ServerAddr is the address of the profiling server.
|
||||||
|
ServerAddr string
|
||||||
|
// AuthUser is the username for basic authentication.
|
||||||
|
AuthUser string `json:",optional"`
|
||||||
|
// AuthPassword is the password for basic authentication.
|
||||||
|
AuthPassword string `json:",optional"`
|
||||||
|
// UploadRate is the duration for which profiling data is uploaded.
|
||||||
|
UploadRate time.Duration `json:",default=15s"`
|
||||||
|
// CheckInterval is the interval to check if profiling should start.
|
||||||
|
CheckInterval time.Duration `json:",default=10s"`
|
||||||
|
// ProfilingDuration is the duration for which profiling data is collected.
|
||||||
|
ProfilingDuration time.Duration `json:",default=2m"`
|
||||||
|
// CpuThreshold the collection is allowed only when the current service cpu < CpuThreshold
|
||||||
|
CpuThreshold int64 `json:",default=700,range=[0:1000)"`
|
||||||
|
|
||||||
|
// ProfileType is the type of profiling to be performed.
|
||||||
|
ProfileType ProfileType
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileType struct {
|
||||||
|
// Logger is a flag to enable or disable logging.
|
||||||
|
Logger bool `json:",default=false"`
|
||||||
|
// CPU is a flag to disable CPU profiling.
|
||||||
|
CPU bool `json:",default=true"`
|
||||||
|
// Goroutines is a flag to disable goroutine profiling.
|
||||||
|
Goroutines bool `json:",default=true"`
|
||||||
|
// Memory is a flag to disable memory profiling.
|
||||||
|
Memory bool `json:",default=true"`
|
||||||
|
// Mutex is a flag to disable mutex profiling.
|
||||||
|
Mutex bool `json:",default=false"`
|
||||||
|
// Block is a flag to disable block profiling.
|
||||||
|
Block bool `json:",default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
profiler interface {
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
}
|
||||||
|
|
||||||
|
pyroscopeProfiler struct {
|
||||||
|
c Config
|
||||||
|
profiler *pyroscope.Profiler
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
|
||||||
|
newProfiler = func(c Config) profiler {
|
||||||
|
return newPyroscopeProfiler(c)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start initializes the pyroscope profiler with the given configuration.
|
||||||
|
func Start(c Config) {
|
||||||
|
// check if the profiling is enabled
|
||||||
|
if len(c.ServerAddr) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default values for the configuration
|
||||||
|
if c.ProfilingDuration <= 0 {
|
||||||
|
c.ProfilingDuration = defaultProfilingDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default values for the configuration
|
||||||
|
if c.CheckInterval <= 0 {
|
||||||
|
c.CheckInterval = defaultCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UploadRate <= 0 {
|
||||||
|
c.UploadRate = defaultUploadRate
|
||||||
|
}
|
||||||
|
|
||||||
|
once.Do(func() {
|
||||||
|
logx.Info("continuous profiling started")
|
||||||
|
|
||||||
|
threading.GoSafe(func() {
|
||||||
|
startPyroscope(c, proc.Done())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// startPyroscope starts the pyroscope profiler with the given configuration.
|
||||||
|
func startPyroscope(c Config, done <-chan struct{}) {
|
||||||
|
var (
|
||||||
|
pr profiler
|
||||||
|
err error
|
||||||
|
latestProfilingTime time.Time
|
||||||
|
intervalTicker = time.NewTicker(c.CheckInterval)
|
||||||
|
profilingTicker = time.NewTicker(c.ProfilingDuration)
|
||||||
|
)
|
||||||
|
|
||||||
|
defer profilingTicker.Stop()
|
||||||
|
defer intervalTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-intervalTicker.C:
|
||||||
|
// Check if the machine is overloaded and if the profiler is not running
|
||||||
|
if pr == nil && isCpuOverloaded(c) {
|
||||||
|
pr = newProfiler(c)
|
||||||
|
if err := pr.Start(); err != nil {
|
||||||
|
logx.Errorf("failed to start profiler: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// record the latest profiling time
|
||||||
|
latestProfilingTime = time.Now()
|
||||||
|
logx.Infof("pyroscope profiler started.")
|
||||||
|
}
|
||||||
|
case <-profilingTicker.C:
|
||||||
|
// check if the profiling duration has passed
|
||||||
|
if !time.Now().After(latestProfilingTime.Add(c.ProfilingDuration)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the profiler is already running, if so, skip
|
||||||
|
if pr != nil {
|
||||||
|
if err = pr.Stop(); err != nil {
|
||||||
|
logx.Errorf("failed to stop profiler: %v", err)
|
||||||
|
}
|
||||||
|
logx.Infof("pyroscope profiler stopped.")
|
||||||
|
pr = nil
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
logx.Infof("continuous profiling stopped.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// genPyroscopeConf generates the pyroscope configuration based on the given config.
|
||||||
|
func genPyroscopeConf(c Config) pyroscope.Config {
|
||||||
|
pConf := pyroscope.Config{
|
||||||
|
UploadRate: c.UploadRate,
|
||||||
|
ApplicationName: c.Name,
|
||||||
|
BasicAuthUser: c.AuthUser, // http basic auth user
|
||||||
|
BasicAuthPassword: c.AuthPassword, // http basic auth password
|
||||||
|
ServerAddress: c.ServerAddr,
|
||||||
|
Logger: nil,
|
||||||
|
HTTPHeaders: map[string]string{},
|
||||||
|
// you can provide static tags via a map:
|
||||||
|
Tags: map[string]string{
|
||||||
|
"name": c.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ProfileType.Logger {
|
||||||
|
pConf.Logger = logx.WithCallerSkip(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ProfileType.CPU {
|
||||||
|
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileCPU)
|
||||||
|
}
|
||||||
|
if c.ProfileType.Goroutines {
|
||||||
|
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileGoroutines)
|
||||||
|
}
|
||||||
|
if c.ProfileType.Memory {
|
||||||
|
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileAllocObjects, pyroscope.ProfileAllocSpace,
|
||||||
|
pyroscope.ProfileInuseObjects, pyroscope.ProfileInuseSpace)
|
||||||
|
}
|
||||||
|
if c.ProfileType.Mutex {
|
||||||
|
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileMutexCount, pyroscope.ProfileMutexDuration)
|
||||||
|
}
|
||||||
|
if c.ProfileType.Block {
|
||||||
|
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileBlockCount, pyroscope.ProfileBlockDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
logx.Infof("applicationName: %s", pConf.ApplicationName)
|
||||||
|
|
||||||
|
return pConf
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCpuOverloaded checks the machine performance based on the given configuration.
|
||||||
|
func isCpuOverloaded(c Config) bool {
|
||||||
|
currentValue := stat.CpuUsage()
|
||||||
|
if currentValue >= c.CpuThreshold {
|
||||||
|
logx.Infof("continuous profiling cpu overload, cpu: %d", currentValue)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPyroscopeProfiler(c Config) profiler {
|
||||||
|
return &pyroscopeProfiler{
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pyroscopeProfiler) Start() error {
|
||||||
|
pConf := genPyroscopeConf(p.c)
|
||||||
|
// set mutex and block profile rate
|
||||||
|
setFraction(p.c)
|
||||||
|
prof, err := pyroscope.Start(pConf)
|
||||||
|
if err != nil {
|
||||||
|
resetFraction(p.c)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.profiler = prof
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pyroscopeProfiler) Stop() error {
|
||||||
|
if p.profiler == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.profiler.Stop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFraction(p.c)
|
||||||
|
p.profiler = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFraction(c Config) {
|
||||||
|
// These 2 lines are only required if you're using mutex or block profiling
|
||||||
|
if c.ProfileType.Mutex {
|
||||||
|
runtime.SetMutexProfileFraction(10) // 10/seconds
|
||||||
|
}
|
||||||
|
if c.ProfileType.Block {
|
||||||
|
runtime.SetBlockProfileRate(1000 * 1000) // 1/millisecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetFraction(c Config) {
|
||||||
|
// These 2 lines are only required if you're using mutex or block profiling
|
||||||
|
if c.ProfileType.Mutex {
|
||||||
|
runtime.SetMutexProfileFraction(0)
|
||||||
|
}
|
||||||
|
if c.ProfileType.Block {
|
||||||
|
runtime.SetBlockProfileRate(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
177
internal/profiling/profiling_test.go
Normal file
177
internal/profiling/profiling_test.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package profiling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/pyroscope-go"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
"github.com/zeromicro/go-zero/core/syncx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStart(t *testing.T) {
|
||||||
|
t.Run("profiling", func(t *testing.T) {
|
||||||
|
var c Config
|
||||||
|
assert.NoError(t, conf.FillDefault(&c))
|
||||||
|
c.Name = "test"
|
||||||
|
p := newProfiler(c)
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
assert.NoError(t, p.Start())
|
||||||
|
assert.NoError(t, p.Stop())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid config", func(t *testing.T) {
|
||||||
|
mp := &mockProfiler{}
|
||||||
|
newProfiler = func(c Config) profiler {
|
||||||
|
return mp
|
||||||
|
}
|
||||||
|
|
||||||
|
Start(Config{})
|
||||||
|
|
||||||
|
Start(Config{
|
||||||
|
ServerAddr: "localhost:4040",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test start profiler", func(t *testing.T) {
|
||||||
|
mp := &mockProfiler{}
|
||||||
|
newProfiler = func(c Config) profiler {
|
||||||
|
return mp
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Config{
|
||||||
|
Name: "test",
|
||||||
|
ServerAddr: "localhost:4040",
|
||||||
|
CheckInterval: time.Millisecond,
|
||||||
|
ProfilingDuration: time.Millisecond * 10,
|
||||||
|
CpuThreshold: 0,
|
||||||
|
}
|
||||||
|
var done = make(chan struct{})
|
||||||
|
go startPyroscope(c, done)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
assert.True(t, mp.started.True())
|
||||||
|
assert.True(t, mp.stopped.True())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test start profiler with cpu overloaded", func(t *testing.T) {
|
||||||
|
mp := &mockProfiler{}
|
||||||
|
newProfiler = func(c Config) profiler {
|
||||||
|
return mp
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Config{
|
||||||
|
Name: "test",
|
||||||
|
ServerAddr: "localhost:4040",
|
||||||
|
CheckInterval: time.Millisecond,
|
||||||
|
ProfilingDuration: time.Millisecond * 10,
|
||||||
|
CpuThreshold: 900,
|
||||||
|
}
|
||||||
|
var done = make(chan struct{})
|
||||||
|
go startPyroscope(c, done)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
assert.False(t, mp.started.True())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("start/stop err", func(t *testing.T) {
|
||||||
|
mp := &mockProfiler{
|
||||||
|
err: assert.AnError,
|
||||||
|
}
|
||||||
|
newProfiler = func(c Config) profiler {
|
||||||
|
return mp
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Config{
|
||||||
|
Name: "test",
|
||||||
|
ServerAddr: "localhost:4040",
|
||||||
|
CheckInterval: time.Millisecond,
|
||||||
|
ProfilingDuration: time.Millisecond * 10,
|
||||||
|
CpuThreshold: 0,
|
||||||
|
}
|
||||||
|
var done = make(chan struct{})
|
||||||
|
go startPyroscope(c, done)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
assert.False(t, mp.started.True())
|
||||||
|
assert.False(t, mp.stopped.True())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenPyroscopeConf(t *testing.T) {
|
||||||
|
c := Config{
|
||||||
|
Name: "",
|
||||||
|
ServerAddr: "localhost:4040",
|
||||||
|
AuthUser: "user",
|
||||||
|
AuthPassword: "password",
|
||||||
|
ProfileType: ProfileType{
|
||||||
|
Logger: true,
|
||||||
|
CPU: true,
|
||||||
|
Goroutines: true,
|
||||||
|
Memory: true,
|
||||||
|
Mutex: true,
|
||||||
|
Block: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pyroscopeConf := genPyroscopeConf(c)
|
||||||
|
assert.Equal(t, c.ServerAddr, pyroscopeConf.ServerAddress)
|
||||||
|
assert.Equal(t, c.AuthUser, pyroscopeConf.BasicAuthUser)
|
||||||
|
assert.Equal(t, c.AuthPassword, pyroscopeConf.BasicAuthPassword)
|
||||||
|
assert.Equal(t, c.Name, pyroscopeConf.ApplicationName)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileCPU)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileGoroutines)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileAllocObjects)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileAllocSpace)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileInuseObjects)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileInuseSpace)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileMutexCount)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileMutexDuration)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileBlockCount)
|
||||||
|
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileBlockDuration)
|
||||||
|
|
||||||
|
setFraction(c)
|
||||||
|
resetFraction(c)
|
||||||
|
|
||||||
|
newPyroscopeProfiler(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPyroscopeProfiler(t *testing.T) {
|
||||||
|
p := newPyroscopeProfiler(Config{})
|
||||||
|
|
||||||
|
assert.Error(t, p.Start())
|
||||||
|
assert.NoError(t, p.Stop())
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockProfiler struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
started syncx.AtomicBool
|
||||||
|
stopped syncx.AtomicBool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockProfiler) Start() error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
if m.err == nil {
|
||||||
|
m.started.Set(true)
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockProfiler) Stop() error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
if m.err == nil {
|
||||||
|
m.stopped.Set(true)
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
@@ -173,17 +173,20 @@ func (s *sseMcpServer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
|
|
||||||
// For notification methods (no ID), we don't send a response
|
// For notification methods (no ID), we don't send a response
|
||||||
isNotification := req.ID == 0
|
isNotification, err := req.isNotification()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request.ID", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
|
||||||
// Special handling for initialization sequence
|
// Special handling for initialization sequence
|
||||||
// Always allow initialize and notifications/initialized regardless of client state
|
// Always allow initialize and notifications/initialized regardless of client state
|
||||||
if req.Method == methodInitialize {
|
if req.Method == methodInitialize {
|
||||||
logx.Infof("Processing initialize request with ID: %d", req.ID)
|
logx.Infof("Processing initialize request with ID: %v", req.ID)
|
||||||
s.processInitialize(r.Context(), client, req)
|
s.processInitialize(r.Context(), client, req)
|
||||||
logx.Infof("Sent initialize response for ID: %d, waiting for notifications/initialized", req.ID)
|
logx.Infof("Sent initialize response for ID: %v, waiting for notifications/initialized", req.ID)
|
||||||
return
|
return
|
||||||
} else if req.Method == methodNotificationsInitialized {
|
} else if req.Method == methodNotificationsInitialized {
|
||||||
// Handle initialized notification
|
// Handle initialized notification
|
||||||
@@ -206,41 +209,41 @@ func (s *sseMcpServer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Process normal requests only after initialization
|
// Process normal requests only after initialization
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case methodToolsCall:
|
case methodToolsCall:
|
||||||
logx.Infof("Received tools call request with ID: %d", req.ID)
|
logx.Infof("Received tools call request with ID: %v", req.ID)
|
||||||
s.processToolCall(r.Context(), client, req)
|
s.processToolCall(r.Context(), client, req)
|
||||||
logx.Infof("Sent tools call response for ID: %d", req.ID)
|
logx.Infof("Sent tools call response for ID: %v", req.ID)
|
||||||
case methodToolsList:
|
case methodToolsList:
|
||||||
logx.Infof("Processing tools/list request with ID: %d", req.ID)
|
logx.Infof("Processing tools/list request with ID: %v", req.ID)
|
||||||
s.processListTools(r.Context(), client, req)
|
s.processListTools(r.Context(), client, req)
|
||||||
logx.Infof("Sent tools/list response for ID: %d", req.ID)
|
logx.Infof("Sent tools/list response for ID: %v", req.ID)
|
||||||
case methodPromptsList:
|
case methodPromptsList:
|
||||||
logx.Infof("Processing prompts/list request with ID: %d", req.ID)
|
logx.Infof("Processing prompts/list request with ID: %v", req.ID)
|
||||||
s.processListPrompts(r.Context(), client, req)
|
s.processListPrompts(r.Context(), client, req)
|
||||||
logx.Infof("Sent prompts/list response for ID: %d", req.ID)
|
logx.Infof("Sent prompts/list response for ID: %v", req.ID)
|
||||||
case methodPromptsGet:
|
case methodPromptsGet:
|
||||||
logx.Infof("Processing prompts/get request with ID: %d", req.ID)
|
logx.Infof("Processing prompts/get request with ID: %v", req.ID)
|
||||||
s.processGetPrompt(r.Context(), client, req)
|
s.processGetPrompt(r.Context(), client, req)
|
||||||
logx.Infof("Sent prompts/get response for ID: %d", req.ID)
|
logx.Infof("Sent prompts/get response for ID: %v", req.ID)
|
||||||
case methodResourcesList:
|
case methodResourcesList:
|
||||||
logx.Infof("Processing resources/list request with ID: %d", req.ID)
|
logx.Infof("Processing resources/list request with ID: %v", req.ID)
|
||||||
s.processListResources(r.Context(), client, req)
|
s.processListResources(r.Context(), client, req)
|
||||||
logx.Infof("Sent resources/list response for ID: %d", req.ID)
|
logx.Infof("Sent resources/list response for ID: %v", req.ID)
|
||||||
case methodResourcesRead:
|
case methodResourcesRead:
|
||||||
logx.Infof("Processing resources/read request with ID: %d", req.ID)
|
logx.Infof("Processing resources/read request with ID: %v", req.ID)
|
||||||
s.processResourcesRead(r.Context(), client, req)
|
s.processResourcesRead(r.Context(), client, req)
|
||||||
logx.Infof("Sent resources/read response for ID: %d", req.ID)
|
logx.Infof("Sent resources/read response for ID: %v", req.ID)
|
||||||
case methodResourcesSubscribe:
|
case methodResourcesSubscribe:
|
||||||
logx.Infof("Processing resources/subscribe request with ID: %d", req.ID)
|
logx.Infof("Processing resources/subscribe request with ID: %v", req.ID)
|
||||||
s.processResourceSubscribe(r.Context(), client, req)
|
s.processResourceSubscribe(r.Context(), client, req)
|
||||||
logx.Infof("Sent resources/subscribe response for ID: %d", req.ID)
|
logx.Infof("Sent resources/subscribe response for ID: %v", req.ID)
|
||||||
case methodPing:
|
case methodPing:
|
||||||
logx.Infof("Processing ping request with ID: %d", req.ID)
|
logx.Infof("Processing ping request with ID: %v", req.ID)
|
||||||
s.processPing(r.Context(), client, req)
|
s.processPing(r.Context(), client, req)
|
||||||
case methodNotificationsCancelled:
|
case methodNotificationsCancelled:
|
||||||
logx.Infof("Received notifications/cancelled notification: %d", req.ID)
|
logx.Infof("Received notifications/cancelled notification: %v", req.ID)
|
||||||
s.processNotificationCancelled(r.Context(), client, req)
|
s.processNotificationCancelled(r.Context(), client, req)
|
||||||
default:
|
default:
|
||||||
logx.Infof("Unknown method: %s from client: %d", req.Method, req.ID)
|
logx.Infof("Unknown method: %s from client: %v", req.Method, req.ID)
|
||||||
s.sendErrorResponse(r.Context(), client, req.ID, "Method not found", errCodeMethodNotFound)
|
s.sendErrorResponse(r.Context(), client, req.ID, "Method not found", errCodeMethodNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -809,7 +812,7 @@ func (s *sseMcpServer) processResourcesRead(ctx context.Context, client *mcpClie
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure MimeType is set if available from the resource definition
|
// Ensure MimeType is set if available from the resource definition
|
||||||
if len(content.MimeType) == 0 && resource.MimeType != "" {
|
if len(content.MimeType) == 0 && len(resource.MimeType) > 0 {
|
||||||
content.MimeType = resource.MimeType
|
content.MimeType = resource.MimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,10 +883,10 @@ func (s *sseMcpServer) processPing(ctx context.Context, client *mcpClient, req R
|
|||||||
|
|
||||||
// sendErrorResponse sends an error response via the SSE channel
|
// sendErrorResponse sends an error response via the SSE channel
|
||||||
func (s *sseMcpServer) sendErrorResponse(ctx context.Context, client *mcpClient,
|
func (s *sseMcpServer) sendErrorResponse(ctx context.Context, client *mcpClient,
|
||||||
id int64, message string, code int) {
|
id any, message string, code int) {
|
||||||
errorResponse := struct {
|
errorResponse := struct {
|
||||||
JsonRpc string `json:"jsonrpc"`
|
JsonRpc string `json:"jsonrpc"`
|
||||||
ID int64 `json:"id"`
|
ID any `json:"id"`
|
||||||
Error errorMessage `json:"error"`
|
Error errorMessage `json:"error"`
|
||||||
}{
|
}{
|
||||||
JsonRpc: jsonRpcVersion,
|
JsonRpc: jsonRpcVersion,
|
||||||
@@ -898,7 +901,7 @@ func (s *sseMcpServer) sendErrorResponse(ctx context.Context, client *mcpClient,
|
|||||||
jsonData, _ := json.Marshal(errorResponse)
|
jsonData, _ := json.Marshal(errorResponse)
|
||||||
// Use CRLF line endings as requested
|
// Use CRLF line endings as requested
|
||||||
sseMessage := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", eventMessage, string(jsonData))
|
sseMessage := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", eventMessage, string(jsonData))
|
||||||
logx.Infof("Sending error for ID %d: %s", id, sseMessage)
|
logx.Infof("Sending error for ID %v: %s", id, sseMessage)
|
||||||
|
|
||||||
// cannot receive from ctx.Done() because we're sending to the channel for SSE messages
|
// cannot receive from ctx.Done() because we're sending to the channel for SSE messages
|
||||||
select {
|
select {
|
||||||
@@ -910,7 +913,7 @@ func (s *sseMcpServer) sendErrorResponse(ctx context.Context, client *mcpClient,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sendResponse sends a success response via the SSE channel
|
// sendResponse sends a success response via the SSE channel
|
||||||
func (s *sseMcpServer) sendResponse(ctx context.Context, client *mcpClient, id int64, result any) {
|
func (s *sseMcpServer) sendResponse(ctx context.Context, client *mcpClient, id any, result any) {
|
||||||
response := Response{
|
response := Response{
|
||||||
JsonRpc: jsonRpcVersion,
|
JsonRpc: jsonRpcVersion,
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -925,13 +928,13 @@ func (s *sseMcpServer) sendResponse(ctx context.Context, client *mcpClient, id i
|
|||||||
|
|
||||||
// Use CRLF line endings as requested
|
// Use CRLF line endings as requested
|
||||||
sseMessage := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", eventMessage, string(jsonData))
|
sseMessage := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", eventMessage, string(jsonData))
|
||||||
logx.Infof("Sending response for ID %d: %s", id, sseMessage)
|
logx.Infof("Sending response for ID %v: %s", id, sseMessage)
|
||||||
|
|
||||||
// cannot receive from ctx.Done() because we're sending to the channel for SSE messages
|
// cannot receive from ctx.Done() because we're sending to the channel for SSE messages
|
||||||
select {
|
select {
|
||||||
case client.channel <- sseMessage:
|
case client.channel <- sseMessage:
|
||||||
default:
|
default:
|
||||||
// Channel buffer is full, log warning and continue
|
// Channel buffer is full, log warning and continue
|
||||||
logx.Infof("Client %s channel is full while sending response with ID %d", client.id, id)
|
logx.Infof("Client %s channel is full while sending response with ID %v", client.id, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,20 @@ func TestHandleRequest_badRequest(t *testing.T) {
|
|||||||
mock.server.handleRequest(w, r)
|
mock.server.handleRequest(w, r)
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("bad id", func(t *testing.T) {
|
||||||
|
mock := newMockMcpServer(t)
|
||||||
|
defer mock.shutdown()
|
||||||
|
|
||||||
|
addTestClient(mock.server, "test-session", true)
|
||||||
|
|
||||||
|
body := `{"jsonrpc": "2.0", "id": {}, "method": "tools.call", "params": {}}`
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/?session_id=test-session", bytes.NewReader([]byte(body)))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
mock.server.handleRequest(w, r)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "Invalid request.ID")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterTool(t *testing.T) {
|
func TestRegisterTool(t *testing.T) {
|
||||||
|
|||||||
31
mcp/types.go
31
mcp/types.go
@@ -3,6 +3,7 @@ package mcp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest"
|
"github.com/zeromicro/go-zero/rest"
|
||||||
@@ -15,11 +16,28 @@ type Cursor string
|
|||||||
type Request struct {
|
type Request struct {
|
||||||
SessionId string `form:"session_id"` // Session identifier for client tracking
|
SessionId string `form:"session_id"` // Session identifier for client tracking
|
||||||
JsonRpc string `json:"jsonrpc"` // Must be "2.0" per JSON-RPC spec
|
JsonRpc string `json:"jsonrpc"` // Must be "2.0" per JSON-RPC spec
|
||||||
ID int64 `json:"id"` // Request identifier for matching responses
|
ID any `json:"id"` // Request identifier for matching responses
|
||||||
Method string `json:"method"` // Method name to invoke
|
Method string `json:"method"` // Method name to invoke
|
||||||
Params json.RawMessage `json:"params"` // Parameters for the method
|
Params json.RawMessage `json:"params"` // Parameters for the method
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r Request) isNotification() (bool, error) {
|
||||||
|
switch val := r.ID.(type) {
|
||||||
|
case int:
|
||||||
|
return val == 0, nil
|
||||||
|
case int64:
|
||||||
|
return val == 0, nil
|
||||||
|
case float64:
|
||||||
|
return val == 0.0, nil
|
||||||
|
case string:
|
||||||
|
return len(val) == 0, nil
|
||||||
|
case nil:
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("invalid type %T", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type PaginatedParams struct {
|
type PaginatedParams struct {
|
||||||
Cursor string `json:"cursor"`
|
Cursor string `json:"cursor"`
|
||||||
Meta struct {
|
Meta struct {
|
||||||
@@ -116,13 +134,8 @@ type FileContent struct {
|
|||||||
|
|
||||||
// EmbeddedResource represents a resource embedded in a message
|
// EmbeddedResource represents a resource embedded in a message
|
||||||
type EmbeddedResource struct {
|
type EmbeddedResource struct {
|
||||||
Type string `json:"type"` // Always "resource"
|
Type string `json:"type"` // Always "resource"
|
||||||
Resource struct {
|
Resource ResourceContent `json:"resource"` // The resource data
|
||||||
URI string `json:"uri"` // Resource URI
|
|
||||||
MimeType string `json:"mimeType"` // MIME type of the resource
|
|
||||||
Text string `json:"text,omitempty"` // Text content (if available)
|
|
||||||
Blob string `json:"blob,omitempty"` // Base64 encoded blob data (if available)
|
|
||||||
} `json:"resource"` // The resource data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotations provides additional metadata for content
|
// Annotations provides additional metadata for content
|
||||||
@@ -249,7 +262,7 @@ type errorObj struct {
|
|||||||
// Response represents a JSON-RPC response
|
// Response represents a JSON-RPC response
|
||||||
type Response struct {
|
type Response struct {
|
||||||
JsonRpc string `json:"jsonrpc"` // Always "2.0"
|
JsonRpc string `json:"jsonrpc"` // Always "2.0"
|
||||||
ID int64 `json:"id"` // Same as request ID
|
ID any `json:"id"` // Same as request ID
|
||||||
Result any `json:"result"` // Result object (null if error)
|
Result any `json:"result"` // Result object (null if error)
|
||||||
Error *errorObj `json:"error,omitempty"` // Error object (null if success)
|
Error *errorObj `json:"error,omitempty"` // Error object (null if success)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package mcp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -55,7 +56,7 @@ func TestRequestUnmarshaling(t *testing.T) {
|
|||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "2.0", req.JsonRpc)
|
assert.Equal(t, "2.0", req.JsonRpc)
|
||||||
assert.Equal(t, int64(789), req.ID)
|
assert.Equal(t, float64(789), req.ID)
|
||||||
assert.Equal(t, "test_method", req.Method)
|
assert.Equal(t, "test_method", req.Method)
|
||||||
|
|
||||||
// Check params unmarshaled correctly
|
// Check params unmarshaled correctly
|
||||||
@@ -204,3 +205,67 @@ func TestCallToolResult(t *testing.T) {
|
|||||||
assert.Contains(t, string(data), `"content":[{"text":"Sample result"}]`)
|
assert.Contains(t, string(data), `"content":[{"text":"Sample result"}]`)
|
||||||
assert.NotContains(t, string(data), `"isError":`)
|
assert.NotContains(t, string(data), `"isError":`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequest_isNotification(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
id any
|
||||||
|
want bool
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
// integer test cases
|
||||||
|
{name: "int zero", id: 0, want: true, wantErr: nil},
|
||||||
|
{name: "int non-zero", id: 1, want: false, wantErr: nil},
|
||||||
|
{name: "int64 zero", id: int64(0), want: true, wantErr: nil},
|
||||||
|
{name: "int64 max", id: int64(9223372036854775807), want: false, wantErr: nil},
|
||||||
|
|
||||||
|
// floating point number test cases
|
||||||
|
{name: "float64 zero", id: float64(0.0), want: true, wantErr: nil},
|
||||||
|
{name: "float64 positive", id: float64(0.000001), want: false, wantErr: nil},
|
||||||
|
{name: "float64 negative", id: float64(-0.000001), want: false, wantErr: nil},
|
||||||
|
{name: "float64 epsilon", id: float64(1e-300), want: false, wantErr: nil},
|
||||||
|
|
||||||
|
// string test cases
|
||||||
|
{name: "empty string", id: "", want: true, wantErr: nil},
|
||||||
|
{name: "non-empty string", id: "abc", want: false, wantErr: nil},
|
||||||
|
{name: "space string", id: " ", want: false, wantErr: nil},
|
||||||
|
{name: "unicode string", id: "こんにちは", want: false, wantErr: nil},
|
||||||
|
|
||||||
|
// special cases
|
||||||
|
{name: "nil", id: nil, want: true, wantErr: nil},
|
||||||
|
|
||||||
|
// logical type test cases
|
||||||
|
{name: "bool true", id: true, want: false, wantErr: errors.New("invalid type bool")},
|
||||||
|
{name: "bool false", id: false, want: false, wantErr: errors.New("invalid type bool")},
|
||||||
|
{name: "struct type", id: struct{}{}, want: false, wantErr: errors.New("invalid type struct {}")},
|
||||||
|
{name: "slice type", id: []int{1, 2, 3}, want: false, wantErr: errors.New("invalid type []int")},
|
||||||
|
{name: "map type", id: map[string]int{"a": 1}, want: false, wantErr: errors.New("invalid type map[string]int")},
|
||||||
|
{name: "pointer type", id: new(int), want: false, wantErr: errors.New("invalid type *int")},
|
||||||
|
{name: "func type", id: func() {}, want: false, wantErr: errors.New("invalid type func()")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := Request{
|
||||||
|
SessionId: "test-session",
|
||||||
|
JsonRpc: "2.0",
|
||||||
|
ID: tt.id,
|
||||||
|
Method: "testMethod",
|
||||||
|
Params: json.RawMessage(`{}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := req.isNotification()
|
||||||
|
|
||||||
|
if (err != nil) != (tt.wantErr != nil) {
|
||||||
|
t.Fatalf("error presence mismatch: got error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() {
|
||||||
|
t.Fatalf("error message mismatch:\ngot %q\nwant %q", err.Error(), tt.wantErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("isNotification() = %v, want %v for ID %v (%T)", got, tt.want, tt.id, tt.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -251,7 +251,3 @@ go-zero enlisted in the [CNCF Cloud Native Landscape](https://landscape.cncf.io/
|
|||||||
## Give a Star! ⭐
|
## Give a Star! ⭐
|
||||||
|
|
||||||
If you like this project or are using it to learn or start your own solution, give it a star to get updates on new releases. Your support matters!
|
If you like this project or are using it to learn or start your own solution, give it a star to get updates on new releases. Your support matters!
|
||||||
|
|
||||||
## Buy me a coffee
|
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/kevwan" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
|
||||||
|
|||||||
@@ -228,6 +228,10 @@ func (ng *engine) getShedder(priority bool) load.Shedder {
|
|||||||
return ng.shedder
|
return ng.shedder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ng *engine) hasTimeout() bool {
|
||||||
|
return ng.conf.Middlewares.Timeout && ng.timeout > 0
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -354,16 +358,17 @@ func (ng *engine) use(middleware Middleware) {
|
|||||||
|
|
||||||
func (ng *engine) withTimeout() internal.StartOption {
|
func (ng *engine) withTimeout() internal.StartOption {
|
||||||
return func(svr *http.Server) {
|
return func(svr *http.Server) {
|
||||||
timeout := ng.timeout
|
if !ng.hasTimeout() {
|
||||||
if timeout > 0 {
|
return
|
||||||
// factor 0.8, to avoid clients send longer content-length than the actual content,
|
|
||||||
// without this timeout setting, the server will time out and respond 503 Service Unavailable,
|
|
||||||
// which triggers the circuit breaker.
|
|
||||||
svr.ReadTimeout = 4 * timeout / 5
|
|
||||||
// factor 1.1, to avoid servers don't have enough time to write responses.
|
|
||||||
// setting the factor less than 1.0 may lead clients not receiving the responses.
|
|
||||||
svr.WriteTimeout = 11 * timeout / 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// factor 0.8, to avoid clients send longer content-length than the actual content,
|
||||||
|
// without this timeout setting, the server will time out and respond 503 Service Unavailable,
|
||||||
|
// which triggers the circuit breaker.
|
||||||
|
svr.ReadTimeout = 4 * ng.timeout / 5
|
||||||
|
// factor 1.1, to avoid servers don't have enough time to write responses.
|
||||||
|
// setting the factor less than 1.0 may lead clients not receiving the responses.
|
||||||
|
svr.WriteTimeout = 11 * ng.timeout / 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,12 @@ func TestEngine_withTimeout(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ng := newEngine(RestConf{Timeout: test.timeout})
|
ng := newEngine(RestConf{
|
||||||
|
Timeout: test.timeout,
|
||||||
|
Middlewares: MiddlewaresConf{
|
||||||
|
Timeout: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
svr := &http.Server{}
|
svr := &http.Server{}
|
||||||
ng.withTimeout()(svr)
|
ng.withTimeout()(svr)
|
||||||
|
|
||||||
@@ -406,6 +411,62 @@ func TestEngine_withTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEngine_ReadWriteTimeout(t *testing.T) {
|
||||||
|
logx.Disable()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
timeout int64
|
||||||
|
middleware bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "0/false",
|
||||||
|
timeout: 0,
|
||||||
|
middleware: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0/true",
|
||||||
|
timeout: 0,
|
||||||
|
middleware: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set/false",
|
||||||
|
timeout: 1000,
|
||||||
|
middleware: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both set",
|
||||||
|
timeout: 1000,
|
||||||
|
middleware: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
ng := newEngine(RestConf{
|
||||||
|
Timeout: test.timeout,
|
||||||
|
Middlewares: MiddlewaresConf{
|
||||||
|
Timeout: test.middleware,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
svr := &http.Server{}
|
||||||
|
ng.withTimeout()(svr)
|
||||||
|
|
||||||
|
assert.Equal(t, time.Duration(0), svr.ReadHeaderTimeout)
|
||||||
|
assert.Equal(t, time.Duration(0), svr.IdleTimeout)
|
||||||
|
|
||||||
|
if test.timeout > 0 && test.middleware {
|
||||||
|
assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*4/5, svr.ReadTimeout)
|
||||||
|
assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*11/10, svr.WriteTimeout)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, time.Duration(0), svr.ReadTimeout)
|
||||||
|
assert.Equal(t, time.Duration(0), svr.WriteTimeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEngine_start(t *testing.T) {
|
func TestEngine_start(t *testing.T) {
|
||||||
logx.Disable()
|
logx.Disable()
|
||||||
|
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ func (h *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
tw.mu.Lock()
|
tw.mu.Lock()
|
||||||
defer tw.mu.Unlock()
|
defer tw.mu.Unlock()
|
||||||
// there isn't any user-defined middleware before TimoutHandler,
|
// there isn't any user-defined middleware before TimeoutHandler,
|
||||||
// so we can guarantee that cancelation in biz related code won't come here.
|
// so we can guarantee that cancellation in biz related code won't come here.
|
||||||
httpx.ErrorCtx(r.Context(), w, ctx.Err(), func(w http.ResponseWriter, err error) {
|
httpx.ErrorCtx(r.Context(), w, ctx.Err(), func(w http.ResponseWriter, err error) {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
w.WriteHeader(statusClientClosedRequest)
|
w.WriteHeader(statusClientClosedRequest)
|
||||||
@@ -151,7 +151,7 @@ func (tw *timeoutWriter) Flush() {
|
|||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns the underline temporary http.Header.
|
// Header returns the underlying temporary http.Header.
|
||||||
func (tw *timeoutWriter) Header() http.Header {
|
func (tw *timeoutWriter) Header() http.Header {
|
||||||
return tw.h
|
return tw.h
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/stringx"
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
apiutil "github.com/zeromicro/go-zero/tools/goctl/api/util"
|
apiutil "github.com/zeromicro/go-zero/tools/goctl/api/util"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
@@ -96,13 +96,13 @@ func (c *componentsContext) createComponent(dir, packetName string, ty spec.Type
|
|||||||
for _, item := range c.responseTypes {
|
for _, item := range c.responseTypes {
|
||||||
if item.Name() == defineStruct.Name() {
|
if item.Name() == defineStruct.Name() {
|
||||||
superClassName = "HttpResponseData"
|
superClassName = "HttpResponseData"
|
||||||
if !stringx.Contains(c.imports, httpResponseData) {
|
if !slices.Contains(c.imports, httpResponseData) {
|
||||||
c.imports = append(c.imports, httpResponseData)
|
c.imports = append(c.imports, httpResponseData)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if superClassName == "HttpData" && !stringx.Contains(c.imports, httpData) {
|
if superClassName == "HttpData" && !slices.Contains(c.imports, httpData) {
|
||||||
c.imports = append(c.imports, httpData)
|
c.imports = append(c.imports, httpData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ func (c *componentsContext) genGetSet(writer io.Writer, indent int) error {
|
|||||||
tyString := javaType
|
tyString := javaType
|
||||||
decorator := ""
|
decorator := ""
|
||||||
javaPrimitiveType := []string{"int", "long", "boolean", "float", "double", "short"}
|
javaPrimitiveType := []string{"int", "long", "boolean", "float", "double", "short"}
|
||||||
if !stringx.Contains(javaPrimitiveType, javaType) {
|
if !slices.Contains(javaPrimitiveType, javaType) {
|
||||||
if member.IsOptional() || member.IsOmitEmpty() {
|
if member.IsOptional() || member.IsOmitEmpty() {
|
||||||
decorator = "@Nullable "
|
decorator = "@Nullable "
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package spec
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/stringx"
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ func (m Member) IsOptional() bool {
|
|||||||
tag := m.Tags()
|
tag := m.Tags()
|
||||||
for _, item := range tag {
|
for _, item := range tag {
|
||||||
if item.Key == bodyTagKey || item.Key == formTagKey {
|
if item.Key == bodyTagKey || item.Key == formTagKey {
|
||||||
if stringx.Contains(item.Options, "optional") {
|
if slices.Contains(item.Options, "optional") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func (m Member) IsOmitEmpty() bool {
|
|||||||
tag := m.Tags()
|
tag := m.Tags()
|
||||||
for _, item := range tag {
|
for _, item := range tag {
|
||||||
if item.Key == bodyTagKey {
|
if item.Key == bodyTagKey {
|
||||||
if stringx.Contains(item.Options, "omitempty") {
|
if slices.Contains(item.Options, "omitempty") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ func (m Member) IsOmitEmpty() bool {
|
|||||||
func (m Member) GetPropertyName() (string, error) {
|
func (m Member) GetPropertyName() (string, error) {
|
||||||
tags := m.Tags()
|
tags := m.Tags()
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
if stringx.Contains(definedKeys, tag.Key) {
|
if slices.Contains(definedKeys, tag.Key) {
|
||||||
if tag.Name == "-" {
|
if tag.Name == "-" {
|
||||||
return util.Untitle(m.Name), nil
|
return util.Untitle(m.Name), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ const (
|
|||||||
propertyKeySchemes = "schemes"
|
propertyKeySchemes = "schemes"
|
||||||
propertyKeyTags = "tags"
|
propertyKeyTags = "tags"
|
||||||
propertyKeySummary = "summary"
|
propertyKeySummary = "summary"
|
||||||
|
propertyKeyGroup = "group"
|
||||||
|
propertyKeyOperationId = "operationId"
|
||||||
propertyKeyDeprecated = "deprecated"
|
propertyKeyDeprecated = "deprecated"
|
||||||
propertyKeyPrefix = "prefix"
|
propertyKeyPrefix = "prefix"
|
||||||
propertyKeyAuthType = "authType"
|
propertyKeyAuthType = "authType"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type (
|
|||||||
summary: "query 类型接口集合" // 对应 swagger 的 summary
|
summary: "query 类型接口集合" // 对应 swagger 的 summary
|
||||||
prefix: v1
|
prefix: v1
|
||||||
authType: apiKey // 指定该路由使用的鉴权类型,值为 securityDefinitionsFromJson 中定义的名称
|
authType: apiKey // 指定该路由使用的鉴权类型,值为 securityDefinitionsFromJson 中定义的名称
|
||||||
|
group:"demo"
|
||||||
)
|
)
|
||||||
service Swagger {
|
service Swagger {
|
||||||
@doc (
|
@doc (
|
||||||
|
|||||||
@@ -34,23 +34,6 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para
|
|||||||
if !ok {
|
if !ok {
|
||||||
return []spec.Parameter{}
|
return []spec.Parameter{}
|
||||||
}
|
}
|
||||||
structName, ok := isPostJson(ctx, method, tp)
|
|
||||||
if ok {
|
|
||||||
return []spec.Parameter{
|
|
||||||
{
|
|
||||||
ParamProps: spec.ParamProps{
|
|
||||||
In: paramsInBody,
|
|
||||||
Name: paramsInBody,
|
|
||||||
Required: true,
|
|
||||||
Schema: &spec.Schema{
|
|
||||||
SchemaProps: spec.SchemaProps{
|
|
||||||
Ref: spec.MustCreateRef(getRefName(structName)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
resp []spec.Parameter
|
resp []spec.Parameter
|
||||||
@@ -197,20 +180,38 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if len(properties) > 0 {
|
if len(properties) > 0 {
|
||||||
resp = append(resp, spec.Parameter{
|
if ctx.UseDefinitions {
|
||||||
ParamProps: spec.ParamProps{
|
structName, ok := isPostJson(ctx, method, tp)
|
||||||
In: paramsInBody,
|
if ok {
|
||||||
Name: paramsInBody,
|
resp = append(resp, spec.Parameter{
|
||||||
Required: true,
|
ParamProps: spec.ParamProps{
|
||||||
Schema: &spec.Schema{
|
In: paramsInBody,
|
||||||
SchemaProps: spec.SchemaProps{
|
Name: paramsInBody,
|
||||||
Type: typeFromGoType(ctx, structType),
|
Required: true,
|
||||||
Properties: properties,
|
Schema: &spec.Schema{
|
||||||
Required: requiredFields,
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Ref: spec.MustCreateRef(getRefName(structName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp = append(resp, spec.Parameter{
|
||||||
|
ParamProps: spec.ParamProps{
|
||||||
|
In: paramsInBody,
|
||||||
|
Name: paramsInBody,
|
||||||
|
Required: true,
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: typeFromGoType(ctx, structType),
|
||||||
|
Properties: properties,
|
||||||
|
Required: requiredFields,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/util/stringx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths {
|
func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths {
|
||||||
@@ -70,6 +71,12 @@ func spec2Path(ctx Context, group apiSpec.Group, route apiSpec.Route) spec.PathI
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
groupName := getStringFromKVOrDefault(group.Annotation.Properties, propertyKeyGroup, "")
|
||||||
|
operationId := route.Handler
|
||||||
|
if len(groupName) > 0 {
|
||||||
|
operationId = stringx.From(groupName + "_" + route.Handler).ToCamel()
|
||||||
|
}
|
||||||
|
operationId = stringx.From(operationId).Untitle()
|
||||||
op := &spec.Operation{
|
op := &spec.Operation{
|
||||||
OperationProps: spec.OperationProps{
|
OperationProps: spec.OperationProps{
|
||||||
Description: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyDescription, ""),
|
Description: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyDescription, ""),
|
||||||
@@ -78,10 +85,11 @@ func spec2Path(ctx Context, group apiSpec.Group, route apiSpec.Route) spec.PathI
|
|||||||
Schemes: getListFromInfoOrDefault(route.AtDoc.Properties, propertyKeySchemes, []string{schemeHttps}),
|
Schemes: getListFromInfoOrDefault(route.AtDoc.Properties, propertyKeySchemes, []string{schemeHttps}),
|
||||||
Tags: getListFromInfoOrDefault(group.Annotation.Properties, propertyKeyTags, getListFromInfoOrDefault(group.Annotation.Properties, propertyKeySummary, []string{})),
|
Tags: getListFromInfoOrDefault(group.Annotation.Properties, propertyKeyTags, getListFromInfoOrDefault(group.Annotation.Properties, propertyKeySummary, []string{})),
|
||||||
Summary: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeySummary, getFirstUsableString(route.AtDoc.Text, route.Handler)),
|
Summary: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeySummary, getFirstUsableString(route.AtDoc.Text, route.Handler)),
|
||||||
|
ID: operationId,
|
||||||
Deprecated: getBoolFromKVOrDefault(route.AtDoc.Properties, propertyKeyDeprecated, false),
|
Deprecated: getBoolFromKVOrDefault(route.AtDoc.Properties, propertyKeyDeprecated, false),
|
||||||
Parameters: parametersFromType(ctx, route.Method, route.RequestType),
|
Parameters: parametersFromType(ctx, route.Method, route.RequestType),
|
||||||
Responses: jsonResponseFromType(ctx, route.AtDoc, route.ResponseType),
|
|
||||||
Security: security,
|
Security: security,
|
||||||
|
Responses: jsonResponseFromType(ctx, route.AtDoc, route.ResponseType),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
externalDocsDescription := getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyExternalDocsDescription, "")
|
externalDocsDescription := getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyExternalDocsDescription, "")
|
||||||
|
|||||||
@@ -24,6 +24,19 @@ func propertiesFromType(ctx Context, tp apiSpec.Type) (spec.SchemaProperties, []
|
|||||||
example, defaultValue any
|
example, defaultValue any
|
||||||
enum []any
|
enum []any
|
||||||
)
|
)
|
||||||
|
pathTag, _ := tag.Get(tagPath)
|
||||||
|
if pathTag != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formTag, _ := tag.Get(tagForm)
|
||||||
|
if formTag != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headerTag, _ := tag.Get(tagHeader)
|
||||||
|
if headerTag != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
jsonTag, _ := tag.Get(tagJson)
|
jsonTag, _ := tag.Get(tagJson)
|
||||||
if jsonTag != nil {
|
if jsonTag != nil {
|
||||||
jsonTagString = jsonTag.Name
|
jsonTagString = jsonTag.Name
|
||||||
|
|||||||
@@ -172,13 +172,12 @@ func sampleTypeFromGoType(ctx Context, tp apiSpec.Type) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func typeContainsTag(_ Context, structType apiSpec.DefineStruct, tag string) bool {
|
func typeContainsTag(ctx Context, structType apiSpec.DefineStruct, tag string) bool {
|
||||||
for _, field := range structType.Members {
|
members := expandMembers(ctx, structType)
|
||||||
tags, _ := apiSpec.Parse(field.Tag)
|
for _, member := range members {
|
||||||
for _, t := range tags.Tags() {
|
tags, _ := apiSpec.Parse(member.Tag)
|
||||||
if t.Key == tag {
|
if _, err := tags.Get(tag); err == nil {
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -294,7 +293,7 @@ func specExtensions(api apiSpec.Info) (spec.Extensions, *spec.Info) {
|
|||||||
ext := spec.Extensions{}
|
ext := spec.Extensions{}
|
||||||
ext.Add("x-goctl-version", version.BuildVersion)
|
ext.Add("x-goctl-version", version.BuildVersion)
|
||||||
ext.Add("x-description", "This is a goctl generated swagger file.")
|
ext.Add("x-description", "This is a goctl generated swagger file.")
|
||||||
ext.Add("x-date", time.Now().Format("2006-01-02 15:04:05"))
|
ext.Add("x-date", time.Now().Format(time.DateTime))
|
||||||
ext.Add("x-github", "https://github.com/zeromicro/go-zero")
|
ext.Add("x-github", "https://github.com/zeromicro/go-zero")
|
||||||
ext.Add("x-go-zero-doc", "https://go-zero.dev/")
|
ext.Add("x-go-zero-doc", "https://go-zero.dev/")
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
APP_NAME=goctl
|
APP_NAME=goctl
|
||||||
APP_VERSION=1.8.4-alpha
|
APP_VERSION=1.8.4-beta
|
||||||
103
tools/goctl/change.md
Normal file
103
tools/goctl/change.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# 1.8.4-beta
|
||||||
|
|
||||||
|
## swagger
|
||||||
|
- [features] Supported operation id for swagger
|
||||||
|
## Other
|
||||||
|
- Updated version to 1.8.4-beta
|
||||||
|
|
||||||
|
|
||||||
|
# 1.8.4-alpha
|
||||||
|
|
||||||
|
## swagger
|
||||||
|
1. [bug fix] remove example generation when request body are `query`, `path` and `header`
|
||||||
|
- it not supported in api spec 2.0
|
||||||
|
- it's will generate example when request body is json format.
|
||||||
|
2. [features] swagger generation supported definitions
|
||||||
|
- supported response definitions
|
||||||
|
- supported json request body definitions
|
||||||
|
- do not support query and form definitions, use parameters instead.
|
||||||
|
|
||||||
|
**How to use?**
|
||||||
|
Use the `useDefinitions` keyword in the info code block of the API file to declare the enable. This value is a boolean value. When set to `true`, it will enable the generation of definitions. Otherwise, it will be generated according to properties, and the default is `false`, for example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
info (
|
||||||
|
...
|
||||||
|
wrapCodeMsg: true
|
||||||
|
useDefinitions: true
|
||||||
|
)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
the demo result of swagger.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
...
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"description": "1001-User not login\u003cbr\u003e1002-User permission denied",
|
||||||
|
"type": "integer",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/FormResp"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"description": "business message",
|
||||||
|
"type": "string",
|
||||||
|
"example": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete API example, please refer to the `api/swagger/example/example.api` file in pr. For a complete swagger result example, please refer to the `api/swagger/example/example.swagger.json` file in pr.
|
||||||
|
|
||||||
|
## 2. `goctl api go` code generation
|
||||||
|
- [bug-fix] Add flag `--type-group` to control the output of types(deprecated: experimental switch control type grouping is no longer used), if true, the types in only one group will separate by file.
|
||||||
|
- example `goctl api go --api demo.api --dir demo --type-group`
|
||||||
|
- use `group` keyword in @server block to define the group name in api file, for example
|
||||||
|
```go
|
||||||
|
@server(
|
||||||
|
group: user
|
||||||
|
)
|
||||||
|
service demo{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
the example of separated types by file
|
||||||
|
```
|
||||||
|
.
|
||||||
|
└── types
|
||||||
|
├── common.go
|
||||||
|
├── gotoolexport.go
|
||||||
|
├── importfile.go
|
||||||
|
├── process.go
|
||||||
|
└── types.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3 API Parser
|
||||||
|
- supported identifier value for info key-value in api parser
|
||||||
|
for example
|
||||||
|
|
||||||
|
```
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
info(
|
||||||
|
enable: true
|
||||||
|
disable: false
|
||||||
|
)
|
||||||
|
...
|
||||||
|
```
|
||||||
@@ -206,6 +206,7 @@
|
|||||||
"short": "Generate mongo model",
|
"short": "Generate mongo model",
|
||||||
"type": "Specified model type name",
|
"type": "Specified model type name",
|
||||||
"cache": "Generate code with cache [optional]",
|
"cache": "Generate code with cache [optional]",
|
||||||
|
"prefix": "Generate code with cache prefix [optional]",
|
||||||
"easy": "Generate code with auto generated CollectionName for easy declare [optional]",
|
"easy": "Generate code with auto generated CollectionName for easy declare [optional]",
|
||||||
"dir": "{{.goctl.model.dir}}",
|
"dir": "{{.goctl.model.dir}}",
|
||||||
"style": "{{.global.style}}",
|
"style": "{{.global.style}}",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// BuildVersion is the version of goctl.
|
// BuildVersion is the version of goctl.
|
||||||
const BuildVersion = "1.8.4-alpha"
|
const BuildVersion = "1.8.4"
|
||||||
|
|
||||||
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}
|
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/stringx"
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/rpc/execx"
|
"github.com/zeromicro/go-zero/tools/goctl/rpc/execx"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/console"
|
"github.com/zeromicro/go-zero/tools/goctl/util/console"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/ctx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/ctx"
|
||||||
@@ -37,7 +37,7 @@ func editMod(version string, verbose bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !stringx.Contains(latest, version) {
|
if !slices.Contains(latest, version) {
|
||||||
return fmt.Errorf("release version %q is not found", version)
|
return fmt.Errorf("release version %q is not found", version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func init() {
|
|||||||
|
|
||||||
mongoCmdFlags.StringSliceVarP(&mongo.VarStringSliceType, "type", "t")
|
mongoCmdFlags.StringSliceVarP(&mongo.VarStringSliceType, "type", "t")
|
||||||
mongoCmdFlags.BoolVarP(&mongo.VarBoolCache, "cache", "c")
|
mongoCmdFlags.BoolVarP(&mongo.VarBoolCache, "cache", "c")
|
||||||
|
mongoCmdFlags.StringVarP(&mongo.VarStringPrefix, "prefix", "p")
|
||||||
mongoCmdFlags.BoolVarP(&mongo.VarBoolEasy, "easy", "e")
|
mongoCmdFlags.BoolVarP(&mongo.VarBoolEasy, "easy", "e")
|
||||||
mongoCmdFlags.StringVarP(&mongo.VarStringDir, "dir", "d")
|
mongoCmdFlags.StringVarP(&mongo.VarStringDir, "dir", "d")
|
||||||
mongoCmdFlags.StringVar(&mongo.VarStringStyle, "style")
|
mongoCmdFlags.StringVar(&mongo.VarStringStyle, "style")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
type Context struct {
|
type Context struct {
|
||||||
Types []string
|
Types []string
|
||||||
Cache bool
|
Cache bool
|
||||||
|
Prefix string
|
||||||
Easy bool
|
Easy bool
|
||||||
Output string
|
Output string
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
@@ -60,6 +61,7 @@ func generateModel(ctx *Context) error {
|
|||||||
"Type": stringx.From(t).Title(),
|
"Type": stringx.From(t).Title(),
|
||||||
"lowerType": stringx.From(t).Untitle(),
|
"lowerType": stringx.From(t).Untitle(),
|
||||||
"Cache": ctx.Cache,
|
"Cache": ctx.Cache,
|
||||||
|
"Prefix": ctx.Prefix,
|
||||||
"version": version.BuildVersion,
|
"version": version.BuildVersion,
|
||||||
}, output, true); err != nil {
|
}, output, true); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ var (
|
|||||||
VarStringDir string
|
VarStringDir string
|
||||||
// VarBoolCache describes whether cache is enabled.
|
// VarBoolCache describes whether cache is enabled.
|
||||||
VarBoolCache bool
|
VarBoolCache bool
|
||||||
|
// VarStringPrefix string describes the prefix for the cache key.
|
||||||
|
VarStringPrefix string
|
||||||
// VarBoolEasy describes whether to generate Collection Name in the code for easy declare.
|
// VarBoolEasy describes whether to generate Collection Name in the code for easy declare.
|
||||||
VarBoolEasy bool
|
VarBoolEasy bool
|
||||||
// VarStringStyle describes the style.
|
// VarStringStyle describes the style.
|
||||||
@@ -35,6 +37,7 @@ var (
|
|||||||
func Action(_ *cobra.Command, _ []string) error {
|
func Action(_ *cobra.Command, _ []string) error {
|
||||||
tp := VarStringSliceType
|
tp := VarStringSliceType
|
||||||
c := VarBoolCache
|
c := VarBoolCache
|
||||||
|
p := VarStringPrefix
|
||||||
easy := VarBoolEasy
|
easy := VarBoolEasy
|
||||||
o := strings.TrimSpace(VarStringDir)
|
o := strings.TrimSpace(VarStringDir)
|
||||||
s := VarStringStyle
|
s := VarStringStyle
|
||||||
@@ -74,6 +77,7 @@ func Action(_ *cobra.Command, _ []string) error {
|
|||||||
return generate.Do(&generate.Context{
|
return generate.Do(&generate.Context{
|
||||||
Types: tp,
|
Types: tp,
|
||||||
Cache: c,
|
Cache: c,
|
||||||
|
Prefix: p,
|
||||||
Easy: easy,
|
Easy: easy,
|
||||||
Output: a,
|
Output: a,
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
)
|
)
|
||||||
|
|
||||||
{{if .Cache}}var prefix{{.Type}}CacheKey = "cache:{{.lowerType}}:"{{end}}
|
{{if .Cache}}var prefix{{.Type}}CacheKey = "{{if .Prefix}}{{.Prefix}}:{{end}}cache:{{.lowerType}}:"{{end}}
|
||||||
|
|
||||||
type {{.lowerType}}Model interface{
|
type {{.lowerType}}Model interface{
|
||||||
Insert(ctx context.Context,data *{{.Type}}) error
|
Insert(ctx context.Context,data *{{.Type}}) error
|
||||||
|
|||||||
@@ -192,3 +192,131 @@ func TestUntitle(t *testing.T) {
|
|||||||
assert.Equal(t, c.want, ret)
|
assert.Equal(t, c.want, ret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainsAny(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
s string
|
||||||
|
runes []rune
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "runes is empty",
|
||||||
|
args: args{
|
||||||
|
s: "test",
|
||||||
|
runes: []rune{},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "s is empty and runes is not empty",
|
||||||
|
args: args{
|
||||||
|
s: "",
|
||||||
|
runes: []rune{'a', 'b', 'c'},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "s contains runes",
|
||||||
|
args: args{
|
||||||
|
s: "hello",
|
||||||
|
runes: []rune{'e', 'f'},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "s does not contain runes",
|
||||||
|
args: args{
|
||||||
|
s: "hello",
|
||||||
|
runes: []rune{'x', 'y'},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "s and runes both have one matching character",
|
||||||
|
args: args{
|
||||||
|
s: "a",
|
||||||
|
runes: []rune{'a'},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "s and runes both have one non-matching character",
|
||||||
|
args: args{
|
||||||
|
s: "a",
|
||||||
|
runes: []rune{'b'},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, ContainsAny(tt.args.s, tt.args.runes...), "ContainsAny(%v, %v)", tt.args.s, tt.args.runes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsWhiteSpace(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
s string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "contains space",
|
||||||
|
args: args{s: "hello world"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains newline",
|
||||||
|
args: args{s: "hello\nworld"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains tab",
|
||||||
|
args: args{s: "hello\tworld"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains form feed",
|
||||||
|
args: args{s: "hello\fworld"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains vertical tab",
|
||||||
|
args: args{s: "hello\vworld"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no whitespace",
|
||||||
|
args: args{s: "helloworld"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
args: args{s: ""},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only whitespace",
|
||||||
|
args: args{s: " \t\n\f\v"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains non-standard whitespace",
|
||||||
|
args: args{s: "hello\u00A0world"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, ContainsWhiteSpace(tt.args.s), "ContainsWhiteSpace(%v)", tt.args.s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/zeromicro/go-zero/core/lang"
|
"github.com/zeromicro/go-zero/core/lang"
|
||||||
"github.com/zeromicro/go-zero/core/mathx"
|
|
||||||
"google.golang.org/grpc/resolver"
|
"google.golang.org/grpc/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ func TestDirectBuilder_Build(t *testing.T) {
|
|||||||
}, cc, resolver.BuildOptions{})
|
}, cc, resolver.BuildOptions{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
size := mathx.MinInt(test, subsetSize)
|
size := min(test, subsetSize)
|
||||||
assert.Equal(t, size, len(cc.state.Addresses))
|
assert.Equal(t, size, len(cc.state.Addresses))
|
||||||
m := make(map[string]lang.PlaceholderType)
|
m := make(map[string]lang.PlaceholderType)
|
||||||
for _, each := range cc.state.Addresses {
|
for _, each := range cc.state.Addresses {
|
||||||
|
|||||||
Reference in New Issue
Block a user