Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.21.13.linux-amd64.tar.gz"
goversion: "https://dl.google.com/go/go1.24.3.linux-amd64.tar.gz"
project_path: "tools/goctl"
binary_name: "goctl"
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md
14 changes: 14 additions & 0 deletions core/breaker/breakers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package breaker
import (
"context"
"sync"

"github.com/zeromicro/go-zero/core/logx"
)

const breakerLimit = 10000

var (
lock sync.RWMutex
breakers = make(map[string]Breaker)
Expand Down Expand Up @@ -70,6 +74,10 @@ func DoWithFallbackAcceptableCtx(ctx context.Context, name string, req func() er
}

// GetBreaker returns the Breaker with the given name.
// When the global registry has reached breakerLimit entries, a new unregistered
// Breaker is returned instead of storing it, preventing unbounded memory growth.
// Breaker names should come from a static, finite set (e.g. service names or
// fixed route patterns) rather than dynamic values such as user IDs or URLs.
func GetBreaker(name string) Breaker {
lock.RLock()
b, ok := breakers[name]
Expand All @@ -81,6 +89,12 @@ func GetBreaker(name string) Breaker {
lock.Lock()
b, ok = breakers[name]
if !ok {
if len(breakers) >= breakerLimit {
logx.Errorf("breaker registry is full (%d entries), returning unregistered breaker for %q",
breakerLimit, name)
lock.Unlock()
Comment on lines +93 to +95
return NewBreaker(WithName(name))
}
b = NewBreaker(WithName(name))
breakers[name] = b
}
Expand Down
18 changes: 15 additions & 3 deletions core/limit/tokenlimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const (
tokenFormat = "{%s}.tokens"
timestampFormat = "{%s}.ts"
pingInterval = time.Millisecond * 100
// monitorMaxWait is the maximum time waitForRedis will poll before giving
// up and resetting the monitor flag so a future request can retry.
monitorMaxWait = time.Minute * 5
)

var (
Expand Down Expand Up @@ -142,17 +145,26 @@ func (lim *TokenLimiter) startMonitor() {

func (lim *TokenLimiter) waitForRedis() {
ticker := time.NewTicker(pingInterval)
deadline := time.NewTimer(monitorMaxWait)
defer func() {
ticker.Stop()
deadline.Stop()
lim.rescueLock.Lock()
lim.monitorStarted = false
lim.rescueLock.Unlock()
}()

for range ticker.C {
if lim.store.Ping() {
atomic.StoreUint32(&lim.redisAlive, 1)
for {
select {
case <-deadline.C:
// Redis did not recover within monitorMaxWait; give up so a
// subsequent request can restart the monitor if needed.
return
case <-ticker.C:
if lim.store.Ping() {
atomic.StoreUint32(&lim.redisAlive, 1)
return
}
Comment on lines +157 to +167
}
}
}
30 changes: 22 additions & 8 deletions core/logx/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ var (
// maxContentLength is used to truncate the log content, 0 for not truncating.
maxContentLength uint32
// use uint32 for atomic operations
disableStat uint32
logLevel uint32
options logOptions
disableStat uint32
logLevel uint32
// slowLogLevel controls slow log output independently from the main log level.
// Defaults to ErrorLevel to preserve backward-compatible behavior.
slowLogLevel = uint32(ErrorLevel)
options logOptions
writer = new(atomicWriter)
setupOnce sync.Once
)
Expand Down Expand Up @@ -327,38 +330,45 @@ func Severef(format string, v ...any) {
}
}

// SetSlowLevel sets the minimum log level required to emit slow logs.
// By default it matches ErrorLevel (backward-compatible). Set to InfoLevel
// to make slow logs visible even when error logs are suppressed.
Comment on lines +333 to +335
func SetSlowLevel(level uint32) {
atomic.StoreUint32(&slowLogLevel, level)
}

// Slow writes v into slow log.
func Slow(v ...any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
writeSlow(fmt.Sprint(v...))
}
}

// Slowf writes v with format into slow log.
func Slowf(format string, v ...any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
writeSlow(fmt.Sprintf(format, v...))
}
}

// Slowfn writes function result into slow log.
// This is useful when the function is expensive to call and slow level disabled.
func Slowfn(fn func() any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
writeSlow(fn())
}
}

// Slowv writes v into slow log with json content.
func Slowv(v any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
writeSlow(v)
}
}

// Sloww writes msg along with fields into slow log.
func Sloww(msg string, fields ...LogField) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
writeSlow(msg, fields...)
}
}
Expand Down Expand Up @@ -548,6 +558,10 @@ func shallLog(level uint32) bool {
return atomic.LoadUint32(&logLevel) <= level
}

func shallSlowLog() bool {
return atomic.LoadUint32(&logLevel) <= atomic.LoadUint32(&slowLogLevel)
}

func shallLogStat() bool {
return atomic.LoadUint32(&disableStat) == 0
}
Expand Down
26 changes: 26 additions & 0 deletions core/logx/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,32 @@ func TestSetLevelWithDuration(t *testing.T) {
assert.Equal(t, 0, w.builder.Len())
}

func TestSetSlowLevel(t *testing.T) {
// shallSlowLog() = logLevel <= slowLogLevel.
// Default slowLogLevel == ErrorLevel preserves backward-compatible behavior.
// Setting slowLogLevel below the current logLevel suppresses slow logs independently.
oldSlowLevel := atomic.LoadUint32(&slowLogLevel)
defer atomic.StoreUint32(&slowLogLevel, oldSlowLevel)

w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)

SetLevel(ErrorLevel) // logLevel = 2
defer SetLevel(DebugLevel)
Comment on lines +716 to +717

// Lower slowLogLevel below logLevel to suppress slow logs independently.
// logLevel(2) <= slowLogLevel(1) → false → suppressed.
SetSlowLevel(InfoLevel)
Slow("should be suppressed")
assert.Equal(t, 0, w.builder.Len())

// Restore: logLevel(2) <= slowLogLevel(2) → true → slow logs emitted again.
SetSlowLevel(ErrorLevel)
Slow("should appear")
assert.True(t, w.builder.Len() > 0)
}

func TestErrorfWithWrappedError(t *testing.T) {
SetLevel(ErrorLevel)
const message = "there"
Expand Down
10 changes: 5 additions & 5 deletions core/logx/richlogger.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,31 +131,31 @@ func (l *richLogger) Infow(msg string, fields ...LogField) {
}

func (l *richLogger) Slow(v ...any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
l.slow(fmt.Sprint(v...))
}
}

func (l *richLogger) Slowf(format string, v ...any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
l.slow(fmt.Sprintf(format, v...))
}
}

func (l *richLogger) Slowfn(fn func() any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
l.slow(fn())
}
}

func (l *richLogger) Slowv(v any) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
l.slow(v)
}
}

func (l *richLogger) Sloww(msg string, fields ...LogField) {
if shallLog(ErrorLevel) {
if shallSlowLog() {
l.slow(msg, fields...)
}
Comment on lines 133 to 160
}
Expand Down
Loading