Compare commits

...

6 Commits

Author SHA1 Message Date
Kevin Wan
bbe5bbb0c0 chore: update go-redis for the retracted versions (#5235) 2025-10-11 16:20:23 +08:00
dependabot[bot]
5ad2278a69 chore(deps): bump go.mongodb.org/mongo-driver/v2 from 2.3.0 to 2.3.1 (#5230)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-10 21:32:04 +08:00
ZH1995
77763fe748 Fix api doc url in readme-cn.md (#5233) 2025-10-10 13:21:46 +00:00
Kevin Wan
538c4fb5c7 fix: issue #5154, not using sse template files (#5220) 2025-10-08 11:56:52 +00:00
Kevin Wan
315fb2fe0a Add company to the list in readme-cn.md (#5222) 2025-10-07 21:04:09 +08:00
Copilot
e382887eb8 docs: Add comprehensive documentation for blocking Redis operations (XReadGroup, Blpop) (#5221)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kevwan <1918356+kevwan@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2025-10-07 16:46:10 +08:00
8 changed files with 259 additions and 10 deletions

View File

@@ -259,12 +259,34 @@ func (s *Redis) BitPosCtx(ctx context.Context, key string, bit, start, end int64
}
// Blpop uses passed in redis connection to execute blocking queries.
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode to avoid
// exhausting the connection pool. Blocking commands hold connections for extended periods and should
// not share the regular connection pool.
//
// Example usage:
//
// node, err := redis.CreateBlockingNode(rds)
// if err != nil {
// // handle error
// }
// defer node.Close()
//
// value, err := rds.Blpop(node, "mylist")
// if err != nil {
// // handle error
// }
//
// Doesn't benefit from pooling redis connections of blocking queries
func (s *Redis) Blpop(node RedisNode, key string) (string, error) {
return s.BlpopCtx(context.Background(), node, key)
}
// BlpopCtx uses passed in redis connection to execute blocking queries.
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode.
// See Blpop for usage examples.
//
// Doesn't benefit from pooling redis connections of blocking queries
func (s *Redis) BlpopCtx(ctx context.Context, node RedisNode, key string) (string, error) {
return s.BlpopWithTimeoutCtx(ctx, node, blockingQueryTimeout, key)
@@ -272,12 +294,18 @@ func (s *Redis) BlpopCtx(ctx context.Context, node RedisNode, key string) (strin
// BlpopEx uses passed in redis connection to execute blpop command.
// The difference against Blpop is that this method returns a bool to indicate success.
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode.
// See Blpop for usage examples.
func (s *Redis) BlpopEx(node RedisNode, key string) (string, bool, error) {
return s.BlpopExCtx(context.Background(), node, key)
}
// BlpopExCtx uses passed in redis connection to execute blpop command.
// The difference against Blpop is that this method returns a bool to indicate success.
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode.
// See Blpop for usage examples.
func (s *Redis) BlpopExCtx(ctx context.Context, node RedisNode, key string) (string, bool, error) {
if node == nil {
return "", false, ErrNilNode
@@ -297,12 +325,18 @@ func (s *Redis) BlpopExCtx(ctx context.Context, node RedisNode, key string) (str
// BlpopWithTimeout uses passed in redis connection to execute blpop command.
// Control blocking query timeout
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode.
// See Blpop for usage examples.
func (s *Redis) BlpopWithTimeout(node RedisNode, timeout time.Duration, key string) (string, error) {
return s.BlpopWithTimeoutCtx(context.Background(), node, timeout, key)
}
// BlpopWithTimeoutCtx uses passed in redis connection to execute blpop command.
// Control blocking query timeout
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode.
// See Blpop for usage examples.
func (s *Redis) BlpopWithTimeoutCtx(ctx context.Context, node RedisNode, timeout time.Duration,
key string) (string, error) {
if node == nil {
@@ -1840,6 +1874,29 @@ func (s *Redis) XInfoStreamCtx(ctx context.Context, stream string) (*red.XInfoSt
// XReadGroup reads messages from Redis streams as part of a consumer group.
// It allows for distributed processing of stream messages with automatic message delivery semantics.
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode to avoid
// exhausting the connection pool. Blocking commands hold connections for extended periods and should
// not share the regular connection pool.
//
// Example usage:
//
// node, err := redis.CreateBlockingNode(rds)
// if err != nil {
// // handle error
// }
// defer node.Close()
//
// streams, err := rds.XReadGroup(
// node, // RedisNode created with CreateBlockingNode
// "mygroup", // consumer group name
// "consumer1", // consumer ID
// 10, // max number of messages to read
// 5*time.Second, // block duration
// false, // noAck flag
// "mystream", // stream name
// )
//
// Doesn't benefit from pooling redis connections of blocking queries.
func (s *Redis) XReadGroup(node RedisNode, group string, consumerId string, count int64,
block time.Duration, noAck bool, streams ...string) ([]red.XStream, error) {
@@ -1847,6 +1904,10 @@ func (s *Redis) XReadGroup(node RedisNode, group string, consumerId string, coun
}
// XReadGroupCtx is the context-aware version of XReadGroup.
//
// For blocking operations, you must create a dedicated RedisNode using CreateBlockingNode to avoid
// exhausting the connection pool. See XReadGroup for usage examples.
//
// Doesn't benefit from pooling redis connections of blocking queries.
func (s *Redis) XReadGroupCtx(ctx context.Context, node RedisNode, group string, consumerId string,
count int64, block time.Duration, noAck bool, streams ...string) ([]red.XStream, error) {

View File

@@ -13,7 +13,37 @@ type ClosableNode interface {
Close()
}
// CreateBlockingNode returns a ClosableNode.
// CreateBlockingNode creates a dedicated RedisNode for blocking operations.
//
// Blocking Redis commands (like BLPOP, BRPOP, XREADGROUP with block parameter) hold connections
// for extended periods while waiting for data. Using them with the regular Redis connection pool
// can exhaust all available connections, causing other operations to fail or timeout.
//
// CreateBlockingNode creates a separate Redis client with a minimal connection pool (size 1) that
// is dedicated to blocking operations. This ensures blocking commands don't interfere with regular
// Redis operations.
//
// Example usage:
//
// rds := redis.MustNewRedis(redis.RedisConf{
// Host: "localhost:6379",
// Type: redis.NodeType,
// })
//
// // Create a dedicated node for blocking operations
// node, err := redis.CreateBlockingNode(rds)
// if err != nil {
// // handle error
// }
// defer node.Close() // Important: close the node when done
//
// // Use the node for blocking operations
// value, err := rds.Blpop(node, "mylist")
// if err != nil {
// // handle error
// }
//
// The returned ClosableNode must be closed when no longer needed to release resources.
func CreateBlockingNode(r *Redis) (ClosableNode, error) {
timeout := readWriteTimeout + blockingQueryTimeout

4
go.mod
View File

@@ -16,12 +16,12 @@ require (
github.com/jhump/protoreflect v1.17.0
github.com/pelletier/go-toml/v2 v2.2.2
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.15.0
github.com/redis/go-redis/v9 v9.14.0
github.com/spaolacci/murmur3 v1.1.0
github.com/stretchr/testify v1.11.1
go.etcd.io/etcd/api/v3 v3.5.15
go.etcd.io/etcd/client/v3 v3.5.15
go.mongodb.org/mongo-driver/v2 v2.3.0
go.mongodb.org/mongo-driver/v2 v2.3.1
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0

8
go.sum
View File

@@ -154,8 +154,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/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.15.0 h1:2jdes0xJxer4h3NUZrZ4OGSntGlXp4WbXju2nOTRXto=
github.com/redis/go-redis/v9 v9.15.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -197,8 +197,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/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU=
go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
go.mongodb.org/mongo-driver/v2 v2.3.1 h1:WrCgSzO7dh1/FrePud9dK5fKNZOE97q5EQimGkos7Wo=
go.mongodb.org/mongo-driver/v2 v2.3.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
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/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=

View File

@@ -175,7 +175,7 @@ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro
* API 文档
[https://go-zero.dev/cn/](https://go-zero.dev/cn/)
[https://go-zero.dev](https://go-zero.dev)
* awesome 系列(更多文章见『微服务实践』公众号)
@@ -305,6 +305,7 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
>107. 深圳市聚货通信息科技有限公司
>108. 浙江银盾云科技有限公司
>109. 南京造世网络科技有限公司
>110. 温州飞儿云信息技术有限公司
如果贵公司也已使用 go-zero欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。

View File

@@ -38,9 +38,11 @@ func genHandler(dir, rootPkg, projectPkg string, cfg *config.Config, group spec.
}
var builtinTemplate = handlerTemplate
var templateFile = handlerTemplateFile
sse := group.GetAnnotation("sse")
if sse == "true" {
builtinTemplate = sseHandlerTemplate
templateFile = sseHandlerTemplateFile
}
return genFile(fileGenConfig{
@@ -49,7 +51,7 @@ func genHandler(dir, rootPkg, projectPkg string, cfg *config.Config, group spec.
filename: filename + ".go",
templateName: "handlerTemplate",
category: category,
templateFile: handlerTemplateFile,
templateFile: templateFile,
builtinTemplate: builtinTemplate,
data: map[string]any{
"PkgName": pkgName,

View File

@@ -61,9 +61,11 @@ func genLogicByRoute(dir, rootPkg, projectPkg string, cfg *config.Config, group
subDir := getLogicFolderPath(group, route)
builtinTemplate := logicTemplate
templateFile := logicTemplateFile
sse := group.GetAnnotation("sse")
if sse == "true" {
builtinTemplate = sseLogicTemplate
templateFile = sseLogicTemplateFile
responseString = "error"
returnString = "return nil"
resp := responseGoTypeName(route, typesPacket)
@@ -80,7 +82,7 @@ func genLogicByRoute(dir, rootPkg, projectPkg string, cfg *config.Config, group
filename: goFile + ".go",
templateName: "logicTemplate",
category: category,
templateFile: logicTemplateFile,
templateFile: templateFile,
builtinTemplate: builtinTemplate,
data: map[string]any{
"pkgName": subDir[strings.LastIndex(subDir, "/")+1:],

View File

@@ -0,0 +1,153 @@
package gogen
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSSEGeneration(t *testing.T) {
// Create a temporary directory for test
dir := t.TempDir()
// Create a test API file with SSE annotation
apiContent := `syntax = "v1"
type SseReq {
Message string ` + "`json:\"message\"`" + `
}
type SseResp {
Data string ` + "`json:\"data\"`" + `
}
@server (
sse: true
)
service Test {
@handler Sse
get /sse (SseReq) returns (SseResp)
}
`
apiFile := filepath.Join(dir, "test.api")
err := os.WriteFile(apiFile, []byte(apiContent), 0644)
assert.NoError(t, err)
// Generate code
err = DoGenProject(apiFile, dir, "gozero", false)
assert.NoError(t, err)
// Read generated handler file
handlerPath := filepath.Join(dir, "internal/handler/ssehandler.go")
handlerContent, err := os.ReadFile(handlerPath)
assert.NoError(t, err)
// Read generated logic file
logicPath := filepath.Join(dir, "internal/logic/sselogic.go")
logicContent, err := os.ReadFile(logicPath)
assert.NoError(t, err)
handlerStr := string(handlerContent)
logicStr := string(logicContent)
// Verify SSE-specific patterns in handler
// Handler should call: err := l.Sse(&req, client)
assert.Contains(t, handlerStr, "err := l.Sse(&req, client)",
"Handler should call logic with client channel parameter")
// Handler should NOT have the regular pattern: resp, err := l.Sse(&req)
assert.NotContains(t, handlerStr, "resp, err := l.Sse(&req)",
"Handler should not use regular pattern with resp return")
// Handler should use threading.GoSafeCtx
assert.Contains(t, handlerStr, "threading.GoSafeCtx",
"Handler should use threading.GoSafeCtx for SSE")
// Handler should create client channel
assert.Contains(t, handlerStr, "client := make(chan",
"Handler should create client channel")
// Verify SSE-specific patterns in logic
// Logic should have signature: Sse(req *types.SseReq, client chan<- *types.SseResp) error
assert.Contains(t, logicStr, "func (l *SseLogic) Sse(req *types.SseReq, client chan<- *types.SseResp) error",
"Logic should have SSE signature with client channel parameter")
// Logic should NOT have regular signature: Sse(req *types.SseReq) (resp *types.SseResp, err error)
assert.NotContains(t, logicStr, "(resp *types.SseResp, err error)",
"Logic should not have regular signature with resp return")
}
func TestNonSSEGeneration(t *testing.T) {
// Create a temporary directory for test
dir := t.TempDir()
// Create a test API file WITHOUT SSE annotation
apiContent := `syntax = "v1"
type SseReq {
Message string ` + "`json:\"message\"`" + `
}
type SseResp {
Data string ` + "`json:\"data\"`" + `
}
service Test {
@handler Sse
get /sse (SseReq) returns (SseResp)
}
`
apiFile := filepath.Join(dir, "test.api")
err := os.WriteFile(apiFile, []byte(apiContent), 0644)
assert.NoError(t, err)
// Generate code
err = DoGenProject(apiFile, dir, "gozero", false)
assert.NoError(t, err)
// Read generated handler file
handlerPath := filepath.Join(dir, "internal/handler/ssehandler.go")
handlerContent, err := os.ReadFile(handlerPath)
assert.NoError(t, err)
// Read generated logic file
logicPath := filepath.Join(dir, "internal/logic/sselogic.go")
logicContent, err := os.ReadFile(logicPath)
assert.NoError(t, err)
handlerStr := string(handlerContent)
logicStr := string(logicContent)
// Verify regular (non-SSE) patterns in handler
// Handler should call: resp, err := l.Sse(&req)
assert.Contains(t, handlerStr, "resp, err := l.Sse(&req)",
"Handler should use regular pattern with resp return")
// Handler should NOT have SSE pattern: err := l.Sse(&req, client)
assert.NotContains(t, handlerStr, "err := l.Sse(&req, client)",
"Handler should not use SSE pattern")
// Handler should NOT use threading.GoSafeCtx
assert.NotContains(t, handlerStr, "threading.GoSafeCtx",
"Handler should not use threading.GoSafeCtx for regular routes")
// Verify regular (non-SSE) patterns in logic
// Logic should have signature: Sse(req *types.SseReq) (resp *types.SseResp, err error)
assert.Contains(t, logicStr, "(resp *types.SseResp, err error)",
"Logic should have regular signature with resp return")
// Logic should NOT have SSE signature with client parameter
linesToCheck := strings.Split(logicStr, "\n")
hasSSESignature := false
for _, line := range linesToCheck {
if strings.Contains(line, "func (l *SseLogic) Sse") && strings.Contains(line, "client chan<-") {
hasSSESignature = true
break
}
}
assert.False(t, hasSSESignature,
"Logic should not have SSE signature with client channel parameter")
}