Update go dependencies (#1972)
This commit is contained in:
Generated
Vendored
Generated
Vendored
Generated
Vendored
+41
-10
@@ -25,6 +25,9 @@ This is a generic middleware to rate-limit HTTP requests.
|
||||
|
||||
**v7.x.x:** Replaced `time/rate` with `embedded time/rate` so that we can support more rate limit headers.
|
||||
|
||||
**v8.x.x:** Address `RemoteIP` vulnerability concern by replacing `SetIPLookups` with `SetIPLookup`, an explicit way to pick the IP address.
|
||||
|
||||
|
||||
## Five Minute Tutorial
|
||||
|
||||
```go
|
||||
@@ -33,7 +36,8 @@ package main
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
func HelloHandler(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -42,7 +46,15 @@ func HelloHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func main() {
|
||||
// Create a request limiter per handler.
|
||||
http.Handle("/", tollbooth.LimitFuncHandler(tollbooth.NewLimiter(1, nil), HelloHandler))
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
// New in version >= 8, you must explicitly define how to pick the IP address.
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
Name: "X-Real-IP",
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
http.Handle("/", tollbooth.LimitFuncHandler(lmt, HelloHandler))
|
||||
http.ListenAndServe(":12345", nil)
|
||||
}
|
||||
```
|
||||
@@ -54,8 +66,8 @@ func main() {
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth/v7/limiter"
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
@@ -66,10 +78,24 @@ func main() {
|
||||
// every token bucket in it will expire 1 hour after it was initially set.
|
||||
lmt = tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Hour})
|
||||
|
||||
// Configure list of places to look for IP address.
|
||||
// By default it's: "RemoteAddr", "X-Forwarded-For", "X-Real-IP"
|
||||
// If your application is behind a proxy, set "X-Forwarded-For" first.
|
||||
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
|
||||
// New in version >= 8, you must explicitly define how to pick the IP address.
|
||||
// If IP address cannot be found, rate limiter will not be activated.
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
// The name of lookup method.
|
||||
// Possible options are: RemoteAddr, X-Forwarded-For, X-Real-IP, CF-Connecting-IP
|
||||
// All other headers are considered unknown and will be ignored.
|
||||
Name: "X-Real-IP",
|
||||
|
||||
// The index position to pick the ip address from a comma separated list.
|
||||
// The index goes from right to left.
|
||||
//
|
||||
// When there are multiple of the same headers,
|
||||
// we will concat them together in the order of first to last seen.
|
||||
// And then we pick the IP using this index position.
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
// In version >= 8, lmt.SetIPLookups and lmt.GetIPLookups are removed.
|
||||
|
||||
// Limit only GET and POST requests.
|
||||
lmt.SetMethods([]string{"GET", "POST"})
|
||||
@@ -89,8 +115,7 @@ func main() {
|
||||
lmt.RemoveHeaderEntries("X-Access-Token", []string{"limitless-token"})
|
||||
|
||||
// By the way, the setters are chainable. Example:
|
||||
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}).
|
||||
SetMethods([]string{"GET", "POST"}).
|
||||
lmt.SetMethods([]string{"GET", "POST"}).
|
||||
SetBasicAuthUsers([]string{"sansa"}).
|
||||
SetBasicAuthUsers([]string{"tyrion"})
|
||||
```
|
||||
@@ -137,6 +162,12 @@ func main() {
|
||||
```go
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
// New in version >= 8, you must explicitly define how to pick the IP address.
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
Name: "X-Forwarded-For",
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
// Set a custom message.
|
||||
lmt.SetMessage("You have reached maximum request limit.")
|
||||
|
||||
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
+26
-27
@@ -5,6 +5,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
// StringInSlice finds needle in a slice of strings.
|
||||
@@ -17,38 +19,35 @@ func StringInSlice(sliceString []string, needle string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoteIP finds IP Address given http.Request struct.
|
||||
func RemoteIP(ipLookups []string, forwardedForIndexFromBehind int, r *http.Request) string {
|
||||
realIP := r.Header.Get("X-Real-IP")
|
||||
forwardedFor := r.Header.Get("X-Forwarded-For")
|
||||
|
||||
for _, lookup := range ipLookups {
|
||||
if lookup == "RemoteAddr" {
|
||||
// 1. Cover the basic use cases for both ipv4 and ipv6
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// 2. Upon error, just return the remote addr.
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
// RemoteIPFromIPLookup picks an ip address explicitly from limiter.IPLookup criteria.
|
||||
// This function is intended to replace RemoteIP function.
|
||||
func RemoteIPFromIPLookup(ipLookup limiter.IPLookup, r *http.Request) string {
|
||||
switch ipLookup.Name {
|
||||
case "RemoteAddr":
|
||||
// 1. Cover the basic use cases for both ipv4 and ipv6
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// 2. Upon error, just return the remote addr.
|
||||
return r.RemoteAddr
|
||||
}
|
||||
if lookup == "X-Forwarded-For" && forwardedFor != "" {
|
||||
// X-Forwarded-For is potentially a list of addresses separated with ","
|
||||
parts := strings.Split(forwardedFor, ",")
|
||||
for i, p := range parts {
|
||||
parts[i] = strings.TrimSpace(p)
|
||||
}
|
||||
return ip
|
||||
|
||||
partIndex := len(parts) - 1 - forwardedForIndexFromBehind
|
||||
if partIndex < 0 {
|
||||
partIndex = 0
|
||||
}
|
||||
case "X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP":
|
||||
ipAddrListCommaSeparated := r.Header.Values(ipLookup.Name)
|
||||
|
||||
return parts[partIndex]
|
||||
ipAddrCommaSeparated := strings.Join(ipAddrListCommaSeparated, ",")
|
||||
|
||||
ips := strings.Split(ipAddrCommaSeparated, ",")
|
||||
for i, p := range ips {
|
||||
ips[i] = strings.TrimSpace(p)
|
||||
}
|
||||
if lookup == "X-Real-IP" && realIP != "" {
|
||||
return realIP
|
||||
|
||||
ipIndex := len(ips) - 1 - ipLookup.IndexFromRight
|
||||
if ipIndex < 0 {
|
||||
ipIndex = 0
|
||||
}
|
||||
|
||||
return ips[ipIndex]
|
||||
}
|
||||
|
||||
return ""
|
||||
Generated
Vendored
+27
-13
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
cache "github.com/go-pkgz/expirable-cache/v3"
|
||||
|
||||
"github.com/didip/tollbooth/v7/internal/time/rate"
|
||||
"github.com/didip/tollbooth/v8/internal/time/rate"
|
||||
)
|
||||
|
||||
// New is a constructor for Limiter.
|
||||
@@ -19,7 +19,6 @@ func New(generalExpirableOptions *ExpirableOptions) *Limiter {
|
||||
SetMessage("You have reached maximum request limit.").
|
||||
SetStatusCode(429).
|
||||
SetOnLimitReached(nil).
|
||||
SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}).
|
||||
SetForwardedForIndexFromBehind(0).
|
||||
SetHeaders(make(map[string][]string)).
|
||||
SetContextValues(make(map[string][]string)).
|
||||
@@ -43,6 +42,18 @@ func New(generalExpirableOptions *ExpirableOptions) *Limiter {
|
||||
return lmt
|
||||
}
|
||||
|
||||
// IPLookup is a config struct to define how users want to pick the remote IP address.
|
||||
type IPLookup struct {
|
||||
// The name of lookup method.
|
||||
// Possible options are: RemoteAddr, X-Forwarded-For, X-Real-IP, CF-Connecting-IP
|
||||
// All other headers are considered unknown and will be ignored.
|
||||
Name string
|
||||
|
||||
// The index position to pick the ip address from a comma separated list.
|
||||
// The index goes from right to left.
|
||||
IndexFromRight int
|
||||
}
|
||||
|
||||
// Limiter is a config struct to limit a particular request handler.
|
||||
type Limiter struct {
|
||||
// Maximum number of requests to limit per second.
|
||||
@@ -66,10 +77,9 @@ type Limiter struct {
|
||||
// An option to write back what you want upon reaching a limit.
|
||||
overrideDefaultResponseWriter bool
|
||||
|
||||
// List of places to look up IP address.
|
||||
// Default is "RemoteAddr", "X-Forwarded-For", "X-Real-IP".
|
||||
// You can rearrange the order as you like.
|
||||
ipLookups []string
|
||||
// Explicitly define how to look up IP address.
|
||||
// This is intended to replace ipLookups
|
||||
explicitIPLookup IPLookup
|
||||
|
||||
forwardedForIndex int
|
||||
|
||||
@@ -270,10 +280,12 @@ func (l *Limiter) ExecOnLimitReached(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// SetOverrideDefaultResponseWriter is a thread-safe way of setting the response writer override variable.
|
||||
func (l *Limiter) SetOverrideDefaultResponseWriter(override bool) {
|
||||
func (l *Limiter) SetOverrideDefaultResponseWriter(override bool) *Limiter {
|
||||
l.Lock()
|
||||
l.overrideDefaultResponseWriter = override
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetOverrideDefaultResponseWriter is a thread-safe way of getting the response writer override variable.
|
||||
@@ -283,20 +295,22 @@ func (l *Limiter) GetOverrideDefaultResponseWriter() bool {
|
||||
return l.overrideDefaultResponseWriter
|
||||
}
|
||||
|
||||
// SetIPLookups is thread-safe way of setting list of places to look up IP address.
|
||||
func (l *Limiter) SetIPLookups(ipLookups []string) *Limiter {
|
||||
// SetIPLookup is thread-safe way of setting an explicit way to look up IP address.
|
||||
// This method is intended to replace SetIPLookups (version 6 or older).
|
||||
func (l *Limiter) SetIPLookup(lookup IPLookup) *Limiter {
|
||||
l.Lock()
|
||||
l.ipLookups = ipLookups
|
||||
l.explicitIPLookup = lookup
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetIPLookups is thread-safe way of getting list of places to look up IP address.
|
||||
func (l *Limiter) GetIPLookups() []string {
|
||||
// GetIPLookup is thread-safe way of getting an explicit way to look up IP address.
|
||||
// This method is intended to replace the old GetIPLookups (version 6 or older).
|
||||
func (l *Limiter) GetIPLookup() IPLookup {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.ipLookups
|
||||
return l.explicitIPLookup
|
||||
}
|
||||
|
||||
// SetIgnoreURL is thread-safe way of setting whenever ignore the URL on rate limit keys
|
||||
Generated
Vendored
Generated
Vendored
+33
-7
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/didip/tollbooth/v7/errors"
|
||||
"github.com/didip/tollbooth/v7/libstring"
|
||||
"github.com/didip/tollbooth/v7/limiter"
|
||||
"github.com/didip/tollbooth/v8/errors"
|
||||
"github.com/didip/tollbooth/v8/libstring"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
// setResponseHeaders configures X-Rate-Limit-Limit and X-Rate-Limit-Duration
|
||||
@@ -37,8 +37,7 @@ func setRateLimitResponseHeaders(lmt *limiter.Limiter, w http.ResponseWriter, to
|
||||
func NewLimiter(max float64, tbOptions *limiter.ExpirableOptions) *limiter.Limiter {
|
||||
return limiter.New(tbOptions).
|
||||
SetMax(max).
|
||||
SetBurst(int(math.Max(1, max))).
|
||||
SetIPLookups([]string{"X-Forwarded-For", "X-Real-IP", "RemoteAddr"})
|
||||
SetBurst(int(math.Max(1, max)))
|
||||
}
|
||||
|
||||
// LimitByKeys keeps track number of request made by keys separated by pipe.
|
||||
@@ -63,7 +62,7 @@ func ShouldSkipLimiter(lmt *limiter.Limiter, r *http.Request) bool {
|
||||
// ---------------------------------
|
||||
// Filter by remote ip
|
||||
// If we are unable to find remoteIP, skip limiter
|
||||
remoteIP := libstring.RemoteIP(lmt.GetIPLookups(), lmt.GetForwardedForIndexFromBehind(), r)
|
||||
remoteIP := libstring.RemoteIPFromIPLookup(lmt.GetIPLookup(), r)
|
||||
remoteIP = libstring.CanonicalizeIP(remoteIP)
|
||||
if remoteIP == "" {
|
||||
return true
|
||||
@@ -195,7 +194,7 @@ func ShouldSkipLimiter(lmt *limiter.Limiter, r *http.Request) bool {
|
||||
|
||||
// BuildKeys generates a slice of keys to rate-limit by given limiter and request structs.
|
||||
func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
|
||||
remoteIP := libstring.RemoteIP(lmt.GetIPLookups(), lmt.GetForwardedForIndexFromBehind(), r)
|
||||
remoteIP := libstring.RemoteIPFromIPLookup(lmt.GetIPLookup(), r)
|
||||
remoteIP = libstring.CanonicalizeIP(remoteIP)
|
||||
path := r.URL.Path
|
||||
sliceKeys := make([][]string, 0)
|
||||
@@ -347,3 +346,30 @@ func LimitHandler(lmt *limiter.Limiter, next http.Handler) http.Handler {
|
||||
func LimitFuncHandler(lmt *limiter.Limiter, nextFunc func(http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return LimitHandler(lmt, http.HandlerFunc(nextFunc))
|
||||
}
|
||||
|
||||
// HTTPMiddleware wraps http.Handler with tollbooth limiter
|
||||
func HTTPMiddleware(lmt *limiter.Limiter) func(http.Handler) http.Handler {
|
||||
// // set IP lookup only if not set
|
||||
if lmt.GetIPLookup().Name == "" {
|
||||
lmt.SetIPLookup(limiter.IPLookup{Name: "RemoteAddr"})
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
http.Error(w, "Context was canceled", http.StatusServiceUnavailable)
|
||||
return
|
||||
default:
|
||||
if httpError := LimitByRequest(lmt, w, r); httpError != nil {
|
||||
lmt.ExecOnLimitReached(w, r)
|
||||
w.Header().Add("Content-Type", lmt.GetMessageContentType())
|
||||
w.WriteHeader(httpError.StatusCode)
|
||||
w.Write([]byte(httpError.Message)) //nolint:gosec // not much we can do here with failed write
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
## tollbooth_chi
|
||||
|
||||
[Chi](https://github.com/pressly/chi) middleware for rate limiting HTTP requests.
|
||||
|
||||
|
||||
## Five Minutes Tutorial
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/didip/tollbooth"
|
||||
"github.com/didip/tollbooth_chi"
|
||||
"github.com/pressly/chi"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a limiter struct.
|
||||
limiter := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(tollbooth_chi.LimitHandler(limiter))
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Hello, world!"))
|
||||
})
|
||||
|
||||
http.ListenAndServe(":12345", r)
|
||||
}
|
||||
```
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package tollbooth_chi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth/v7/limiter"
|
||||
)
|
||||
|
||||
func LimitHandler(lmt *limiter.Limiter) func(http.Handler) http.Handler {
|
||||
return func(handler http.Handler) http.Handler {
|
||||
wrapper := &limiterWrapper{
|
||||
lmt: lmt,
|
||||
}
|
||||
|
||||
wrapper.handler = handler
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
|
||||
type limiterWrapper struct {
|
||||
lmt *limiter.Limiter
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (l *limiterWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
http.Error(w, "Context was canceled", http.StatusServiceUnavailable)
|
||||
return
|
||||
|
||||
default:
|
||||
httpError := tollbooth.LimitByRequest(l.lmt, w, r)
|
||||
if httpError != nil {
|
||||
l.lmt.ExecOnLimitReached(w, r)
|
||||
w.Header().Add("Content-Type", l.lmt.GetMessageContentType())
|
||||
w.WriteHeader(httpError.StatusCode)
|
||||
w.Write([]byte(httpError.Message))
|
||||
return
|
||||
}
|
||||
|
||||
l.handler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
+9
-8
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-pkgz/auth/v2/avatar"
|
||||
"github.com/go-pkgz/auth/v2/token"
|
||||
)
|
||||
|
||||
@@ -71,15 +72,15 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// setAvatar saves avatar and puts proxied URL to u.Picture
|
||||
func setAvatar(ava AvatarSaver, u token.User, client *http.Client) (token.User, error) {
|
||||
if ava != nil {
|
||||
avatarURL, e := ava.Put(u, client)
|
||||
if e != nil {
|
||||
return u, fmt.Errorf("failed to save avatar for: %w", e)
|
||||
}
|
||||
u.Picture = avatarURL
|
||||
return u, nil
|
||||
if ava == nil || ava == (*avatar.Proxy)(nil) {
|
||||
return u, nil // empty AvatarSaver ok, just skipped
|
||||
}
|
||||
return u, nil // empty AvatarSaver ok, just skipped
|
||||
avatarURL, e := ava.Put(u, client)
|
||||
if e != nil {
|
||||
return u, fmt.Errorf("failed to save avatar for: %w", e)
|
||||
}
|
||||
u.Picture = avatarURL
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func randToken() (string, error) {
|
||||
|
||||
+35
-39
@@ -1,57 +1,53 @@
|
||||
version: "2"
|
||||
run:
|
||||
timeout: 5m
|
||||
output:
|
||||
format: tab
|
||||
skip-dirs:
|
||||
- vendor
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
- singleCaseSwitch
|
||||
- ifElseChain
|
||||
|
||||
concurrency: 4
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- dupl
|
||||
- exportloopref
|
||||
- gas
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gosimple
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- megacheck
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- revive
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
fast: false
|
||||
disable-all: true
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
- singleCaseSwitch
|
||||
- ifElseChain
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- vendor
|
||||
- third_party
|
||||
- builtin
|
||||
- examples
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ func (a *loginAuth) Start(server *smtp.ServerInfo) (proto string, toServer []byt
|
||||
return "LOGIN", []byte(a.user), nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) {
|
||||
func (a *loginAuth) Next(_ []byte, more bool) (toServer []byte, err error) {
|
||||
if more {
|
||||
return []byte(a.password), nil
|
||||
}
|
||||
|
||||
+25
-12
@@ -13,10 +13,12 @@ import (
|
||||
"mime/quotedprintable"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -30,10 +32,10 @@ type Sender struct {
|
||||
logger Logger
|
||||
host string // SMTP host
|
||||
port int // SMTP port
|
||||
contentType string // Content type, optional. Will trigger MIME and Content-Type headers
|
||||
contentType string // content type, optional. Will trigger MIME and Content-Type headers
|
||||
tls bool // TLS auth
|
||||
starttls bool // StartTLS
|
||||
insecureSkipVerify bool // Insecure Skip Verify
|
||||
starttls bool // startTLS
|
||||
insecureSkipVerify bool // insecure Skip Verify
|
||||
smtpUserName string // username
|
||||
smtpPassword string // password
|
||||
authMethod authMethod // auth method
|
||||
@@ -44,12 +46,12 @@ type Sender struct {
|
||||
|
||||
// Params contains all user-defined parameters to send emails
|
||||
type Params struct {
|
||||
From string // From email field
|
||||
To []string // From email field
|
||||
Subject string // Email subject
|
||||
From string // from email field
|
||||
To []string // from email field
|
||||
Subject string // email subject
|
||||
UnsubscribeLink string // POST, https://support.google.com/mail/answer/81126 -> "Use one-click unsubscribe"
|
||||
InReplyTo string // Identifier for email group (category), used for email grouping
|
||||
Attachments []string // Attachments path
|
||||
InReplyTo string // identifier for email group (category), used for email grouping
|
||||
Attachments []string // attachments path
|
||||
InlineImages []string // InlineImages images path
|
||||
}
|
||||
|
||||
@@ -130,12 +132,12 @@ func (em *Sender) Send(text string, params Params) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(params.From); err != nil {
|
||||
if err := client.Mail(extractEmailAddress(params.From)); err != nil {
|
||||
return fmt.Errorf("bad from address %q: %w", params.From, err)
|
||||
}
|
||||
|
||||
for _, rcpt := range params.To {
|
||||
if err := client.Rcpt(rcpt); err != nil {
|
||||
if err := client.Rcpt(extractEmailAddress(rcpt)); err != nil {
|
||||
return fmt.Errorf("bad to address %q: %w", params.To, err)
|
||||
}
|
||||
}
|
||||
@@ -165,13 +167,24 @@ func (em *Sender) Send(text string, params Params) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractEmailAddress extracts the email address from a string that may contain a display name.
|
||||
// For example, it converts `"John Doe" <john@example.com>` to `john@example.com`.
|
||||
// If parsing fails, it returns the original string unchanged.
|
||||
func extractEmailAddress(from string) string {
|
||||
addr, err := mail.ParseAddress(strings.TrimSpace(from))
|
||||
if err != nil {
|
||||
return from
|
||||
}
|
||||
return addr.Address
|
||||
}
|
||||
|
||||
func (em *Sender) String() string {
|
||||
return fmt.Sprintf("smtp://%s:%d, auth:%v, tls:%v, starttls:%v, insecureSkipVerify:%v, timeout:%v, content-type:%q, charset:%q",
|
||||
em.host, em.port, em.smtpUserName != "", em.tls, em.starttls, em.insecureSkipVerify, em.timeOut, em.contentType, em.contentCharset)
|
||||
}
|
||||
|
||||
func (em *Sender) client() (c *smtp.Client, err error) {
|
||||
srvAddress := fmt.Sprintf("%s:%d", em.host, em.port)
|
||||
srvAddress := net.JoinHostPort(em.host, strconv.Itoa(em.port))
|
||||
// #nosec G402
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: em.insecureSkipVerify, // #nosec G402
|
||||
@@ -366,4 +379,4 @@ func (em *Sender) writeFiles(mp *multipart.Writer, files []string, disposition s
|
||||
|
||||
type nopLogger struct{}
|
||||
|
||||
func (nopLogger) Logf(format string, args ...interface{}) {}
|
||||
func (nopLogger) Logf(_ string, _ ...interface{}) {}
|
||||
|
||||
+17
-21
@@ -94,13 +94,12 @@ func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) {
|
||||
// Returns false if there was no eviction: the item was already in the cache,
|
||||
// or the size was not exceeded.
|
||||
func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted bool) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
now := time.Now()
|
||||
if ttl == 0 {
|
||||
ttl = c.ttl
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
@@ -117,7 +116,10 @@ func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted
|
||||
|
||||
// Remove the oldest entry if it is expired, only in case of non-default TTL.
|
||||
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
|
||||
c.removeOldestIfExpired()
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil && now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
evict := c.maxKeys > 0 && len(c.items) > c.maxKeys
|
||||
@@ -197,15 +199,14 @@ func (c *cacheImpl[K, V]) Keys() []K {
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
// Expired entries are filtered out.
|
||||
func (c *cacheImpl[K, V]) Values() []V {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
values := make([]V, 0, len(c.items))
|
||||
now := time.Now()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
continue
|
||||
if !now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
values = append(values, ent.Value.(*cacheItem[K, V]).value)
|
||||
}
|
||||
values = append(values, ent.Value.(*cacheItem[K, V]).value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -291,11 +292,14 @@ func (c *cacheImpl[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
|
||||
// DeleteExpired clears cache of expired items
|
||||
func (c *cacheImpl[K, V]) DeleteExpired() {
|
||||
now := time.Now()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
for _, key := range c.keys() {
|
||||
if time.Now().After(c.items[key].Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(c.items[key])
|
||||
var nextEnt *list.Element
|
||||
for ent := c.evictList.Back(); ent != nil; ent = nextEnt {
|
||||
nextEnt = ent.Prev()
|
||||
if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,14 +348,6 @@ func (c *cacheImpl[K, V]) removeOldest() {
|
||||
}
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache in case it's already expired. Has to be called with lock!
|
||||
func (c *cacheImpl[K, V]) removeOldestIfExpired() {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil && time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
|
||||
func (c *cacheImpl[K, V]) removeElement(e *list.Element) {
|
||||
c.evictList.Remove(e)
|
||||
|
||||
+61
-55
@@ -1,62 +1,68 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
- singleCaseSwitch
|
||||
- ifElseChain
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- staticcheck
|
||||
- gosec
|
||||
- unused
|
||||
- gocyclo
|
||||
- bodyclose
|
||||
- copyloopvar
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
- gocognit
|
||||
- gocritic
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- gosimple
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- whitespace
|
||||
fast: false
|
||||
disable-all: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- text: "at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- stylecheck
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec
|
||||
- dupl
|
||||
exclude-use-default: false
|
||||
exclude-dirs:
|
||||
- vendor
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
revive:
|
||||
enable-all-rules: true
|
||||
rules:
|
||||
- name: unused-receiver
|
||||
disabled: true
|
||||
- name: line-length-limit
|
||||
disabled: true
|
||||
- name: add-constant
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: nested-structs
|
||||
disabled: true
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- hugeParam
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+11
-11
@@ -13,17 +13,17 @@ import (
|
||||
|
||||
// SMTPParams contain settings for smtp server connection
|
||||
type SMTPParams struct {
|
||||
Host string // SMTP host
|
||||
Port int // SMTP port
|
||||
TLS bool // TLS auth
|
||||
StartTLS bool // StartTLS auth
|
||||
InsecureSkipVerify bool // skip certificate verification
|
||||
ContentType string // Content type
|
||||
Charset string // Character set
|
||||
LoginAuth bool // LOGIN auth method instead of default PLAIN, needed for Office 365 and outlook.com
|
||||
Username string // username
|
||||
Password string // password
|
||||
TimeOut time.Duration // TCP connection timeout
|
||||
Host string // SMTP host
|
||||
Port int // SMTP port
|
||||
TLS bool // TLS auth
|
||||
StartTLS bool // StartTLS auth
|
||||
InsecureSkipVerify bool // skip certificate verification
|
||||
ContentType string // Content type
|
||||
Charset string // Character set
|
||||
LoginAuth bool // LOGIN auth method instead of default PLAIN, needed for Office 365 and outlook.com
|
||||
Username string // username
|
||||
Password string // password
|
||||
TimeOut time.Duration // TCP connection timeout
|
||||
}
|
||||
|
||||
// Email notifications client
|
||||
|
||||
+3
-3
@@ -93,9 +93,9 @@ func (s *Slack) findChannelIDByName(name string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
if channel.Name == name {
|
||||
return channel.ID, nil
|
||||
for i := range channels {
|
||||
if channels[i].Name == name {
|
||||
return channels[i].ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -170,23 +170,23 @@ func adjustHTMLTags(htmlText string) string {
|
||||
switch token.Data {
|
||||
case "h1", "h2", "h3":
|
||||
if token.Type == html.StartTagToken {
|
||||
buff.WriteString("<b>")
|
||||
_, _ = buff.WriteString("<b>")
|
||||
}
|
||||
if token.Type == html.EndTagToken {
|
||||
buff.WriteString("</b>")
|
||||
_, _ = buff.WriteString("</b>")
|
||||
}
|
||||
case "h4", "h5", "h6":
|
||||
if token.Type == html.StartTagToken {
|
||||
buff.WriteString("<i><b>")
|
||||
_, _ = buff.WriteString("<i><b>")
|
||||
}
|
||||
if token.Type == html.EndTagToken {
|
||||
buff.WriteString("</b></i>")
|
||||
_, _ = buff.WriteString("</b></i>")
|
||||
}
|
||||
default:
|
||||
buff.WriteString(token.String())
|
||||
_, _ = buff.WriteString(token.String())
|
||||
}
|
||||
default:
|
||||
buff.WriteString(token.String())
|
||||
_, _ = buff.WriteString(token.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,7 +435,7 @@ func (t *Telegram) botInfo(ctx context.Context) (*TelegramBotInfo, error) {
|
||||
}
|
||||
|
||||
// Request makes a request to the Telegram API and return the result
|
||||
func (t *Telegram) Request(ctx context.Context, method string, b []byte, data interface{}) error {
|
||||
func (t *Telegram) Request(ctx context.Context, method string, b []byte, data any) error {
|
||||
return repeater.NewDefault(3, time.Millisecond*250).Do(ctx, func() error {
|
||||
url := fmt.Sprintf("%s%s/%s", t.apiPrefix, t.Token, method)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
version: "2"
|
||||
run:
|
||||
concurrency: 4
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- errorlint
|
||||
- exptostd
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- nakedret
|
||||
- nilerr
|
||||
- prealloc
|
||||
- predeclared
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- thelper
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- nestif
|
||||
- wrapcheck
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- linters:
|
||||
- gosec
|
||||
text: 'G114: Use of net/http serve function that has no support for setting timeouts'
|
||||
- linters:
|
||||
- revive
|
||||
- unparam
|
||||
path: _test\.go$
|
||||
text: unused-parameter
|
||||
- linters:
|
||||
- prealloc
|
||||
path: _test\.go$
|
||||
text: Consider pre-allocating
|
||||
- linters:
|
||||
- gosec
|
||||
- intrange
|
||||
path: _test\.go$
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
# Repeater
|
||||
|
||||
[](https://github.com/go-pkgz/repeater/actions) [](https://goreportcard.com/report/github.com/go-pkgz/repeater) [](https://coveralls.io/github/go-pkgz/repeater?branch=master)
|
||||
|
||||
Package repeater implements a functional mechanism to repeat operations with different retry strategies.
|
||||
|
||||
## Install and update
|
||||
|
||||
`go get -u github.com/go-pkgz/repeater`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example with Exponential Backoff
|
||||
|
||||
```go
|
||||
// create repeater with exponential backoff
|
||||
r := repeater.NewBackoff(5, time.Second) // 5 attempts starting with 1s delay
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// do something that may fail
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Fixed Delay with Critical Error
|
||||
|
||||
```go
|
||||
// create repeater with fixed delay
|
||||
r := repeater.NewFixed(3, 100*time.Millisecond)
|
||||
|
||||
criticalErr := errors.New("critical error")
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// do something that may fail
|
||||
return fmt.Errorf("temp error")
|
||||
}, criticalErr) // will stop immediately if criticalErr returned
|
||||
```
|
||||
|
||||
### Custom Backoff Strategy
|
||||
|
||||
```go
|
||||
r := repeater.NewBackoff(5, time.Second,
|
||||
repeater.WithMaxDelay(10*time.Second),
|
||||
repeater.WithBackoffType(repeater.BackoffLinear),
|
||||
repeater.WithJitter(0.1),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// do something that may fail
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Stop on Any Error
|
||||
|
||||
```go
|
||||
r := repeater.NewFixed(3, time.Millisecond)
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
return errors.New("some error")
|
||||
}, repeater.ErrAny) // will stop on any error
|
||||
```
|
||||
|
||||
## Strategies
|
||||
|
||||
The package provides several retry strategies:
|
||||
|
||||
1. **Fixed Delay** - each retry happens after a fixed time interval
|
||||
2. **Backoff** - delay between retries increases according to the chosen algorithm:
|
||||
- Constant - same delay between attempts
|
||||
- Linear - delay increases linearly
|
||||
- Exponential - delay doubles with each attempt
|
||||
|
||||
Backoff strategy can be customized with:
|
||||
- Maximum delay cap
|
||||
- Jitter to prevent thundering herd
|
||||
- Different backoff types (constant/linear/exponential)
|
||||
|
||||
### Custom Strategies
|
||||
|
||||
You can implement your own retry strategy by implementing the Strategy interface:
|
||||
|
||||
```go
|
||||
type Strategy interface {
|
||||
// NextDelay returns delay for the next attempt
|
||||
// attempt starts from 1
|
||||
NextDelay(attempt int) time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
Example of a custom strategy that increases delay by a custom factor:
|
||||
|
||||
```go
|
||||
// CustomStrategy implements Strategy with custom factor-based delays
|
||||
type CustomStrategy struct {
|
||||
Initial time.Duration
|
||||
Factor float64
|
||||
}
|
||||
|
||||
func (s CustomStrategy) NextDelay(attempt int) time.Duration {
|
||||
if attempt <= 0 {
|
||||
return 0
|
||||
}
|
||||
delay := time.Duration(float64(s.Initial) * math.Pow(s.Factor, float64(attempt-1)))
|
||||
return delay
|
||||
}
|
||||
|
||||
// Usage
|
||||
strategy := &CustomStrategy{Initial: time.Second, Factor: 1.5}
|
||||
r := repeater.NewWithStrategy(5, strategy)
|
||||
err := r.Do(ctx, func() error {
|
||||
// attempts will be delayed by: 1s, 1.5s, 2.25s, 3.37s, 5.06s
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
For backoff strategy, several options are available:
|
||||
|
||||
```go
|
||||
WithMaxDelay(time.Duration) // set maximum delay between retries
|
||||
WithBackoffType(BackoffType) // set backoff type (constant/linear/exponential)
|
||||
WithJitter(float64) // add randomness to delays (0-1.0)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Stops on context cancellation
|
||||
- Can stop on specific errors (pass them as additional parameters to Do)
|
||||
- Special `ErrAny` to stop on any error
|
||||
- Returns last error if all attempts fail
|
||||
- Custom error classification via `SetErrorClassifier`
|
||||
|
||||
### Error Classification
|
||||
|
||||
You can provide a custom error classifier function to dynamically determine if an error should trigger a retry or stop immediately. This is particularly useful for API clients where different error types require different handling:
|
||||
|
||||
```go
|
||||
// Define what errors are retryable
|
||||
isRetryable := func(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
|
||||
// Retryable patterns
|
||||
if strings.Contains(errStr, "429") ||
|
||||
strings.Contains(errStr, "rate limit") ||
|
||||
strings.Contains(errStr, "timeout") ||
|
||||
strings.Contains(errStr, "503") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Non-retryable patterns
|
||||
if strings.Contains(errStr, "401") ||
|
||||
strings.Contains(errStr, "authentication") ||
|
||||
strings.Contains(errStr, "token limit") {
|
||||
return false
|
||||
}
|
||||
|
||||
return true // default to retry
|
||||
}
|
||||
|
||||
// Use with any repeater strategy
|
||||
r := repeater.NewBackoff(5, time.Second)
|
||||
r.SetErrorClassifier(isRetryable)
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// API call that might fail
|
||||
return apiClient.Call()
|
||||
})
|
||||
```
|
||||
|
||||
When an error classifier is set:
|
||||
- After each error, the classifier function is called
|
||||
- If it returns `false`, the operation stops immediately
|
||||
- If it returns `true`, the retry logic continues
|
||||
- The classifier takes precedence over the critical errors list
|
||||
|
||||
This feature works with all repeater strategies (NewFixed, NewBackoff, NewWithStrategy).
|
||||
|
||||
## Execution Statistics
|
||||
|
||||
The repeater tracks execution statistics that can be accessed after calling `Do()`:
|
||||
|
||||
```go
|
||||
r := repeater.NewFixed(5, 100*time.Millisecond)
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// operation that might fail
|
||||
return someOperation()
|
||||
})
|
||||
|
||||
// Get execution statistics
|
||||
stats := r.Stats()
|
||||
|
||||
fmt.Printf("Attempts: %d\n", stats.Attempts)
|
||||
fmt.Printf("Success: %v\n", stats.Success)
|
||||
fmt.Printf("Total Duration: %v\n", stats.TotalDuration)
|
||||
fmt.Printf("Work Duration: %v\n", stats.WorkDuration)
|
||||
fmt.Printf("Delay Duration: %v\n", stats.DelayDuration)
|
||||
if stats.LastError != nil {
|
||||
fmt.Printf("Last Error: %v\n", stats.LastError)
|
||||
}
|
||||
```
|
||||
|
||||
### Available Statistics
|
||||
|
||||
The `Stats` struct provides the following information:
|
||||
|
||||
- `Attempts` - Number of attempts made (including successful ones)
|
||||
- `Success` - Whether the operation eventually succeeded
|
||||
- `TotalDuration` - Total elapsed time from start to finish
|
||||
- `WorkDuration` - Time spent executing the function (excluding delays)
|
||||
- `DelayDuration` - Time spent in delays between attempts
|
||||
- `LastError` - Last error encountered (nil if succeeded)
|
||||
- `StartedAt` - When the repeater started
|
||||
- `FinishedAt` - When the repeater finished
|
||||
|
||||
### Usage Example
|
||||
|
||||
```go
|
||||
r := repeater.NewBackoff(3, time.Second)
|
||||
|
||||
start := time.Now()
|
||||
err := r.Do(ctx, func() error {
|
||||
// Simulate work that takes time
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Randomly fail
|
||||
if rand.Float32() < 0.7 {
|
||||
return errors.New("temporary error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
stats := r.Stats()
|
||||
|
||||
// Log detailed statistics
|
||||
log.Printf("Operation completed in %v with %d attempts",
|
||||
stats.TotalDuration, stats.Attempts)
|
||||
log.Printf("Time spent working: %v", stats.WorkDuration)
|
||||
log.Printf("Time spent waiting: %v", stats.DelayDuration)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed after %d attempts: %v", stats.Attempts, err)
|
||||
} else {
|
||||
log.Printf("Succeeded after %d attempts", stats.Attempts)
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Safety
|
||||
|
||||
Note that the `Repeater` is not thread-safe. Each `Repeater` instance should not be used concurrently for different functions. Create separate `Repeater` instances for concurrent operations.
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Package repeater implements retry functionality with different strategies.
|
||||
// It provides fixed delays and various backoff strategies (constant, linear, exponential) with jitter support.
|
||||
// The package allows custom retry strategies and error-specific handling. Context-aware implementation
|
||||
// supports cancellation and timeouts.
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrAny is a special sentinel error that, when passed as a critical error to Do,
|
||||
// makes it fail on any error from the function
|
||||
var ErrAny = errors.New("any error")
|
||||
|
||||
// ErrorClassifier determines if an error should be retried.
|
||||
// Returns true if the error should trigger a retry, false to stop immediately.
|
||||
type ErrorClassifier func(error) bool
|
||||
|
||||
// Stats holds execution statistics for a repeater run
|
||||
type Stats struct {
|
||||
LastError error // last error encountered (nil if succeeded)
|
||||
StartedAt time.Time // when the repeater started
|
||||
FinishedAt time.Time // when the repeater finished
|
||||
TotalDuration time.Duration // total elapsed time from start to finish
|
||||
WorkDuration time.Duration // time spent executing the function (excluding delays)
|
||||
DelayDuration time.Duration // time spent in delays between attempts
|
||||
Attempts int // number of attempts made (including the successful one)
|
||||
Success bool // whether the operation eventually succeeded
|
||||
}
|
||||
|
||||
// Repeater holds configuration for retry operations.
|
||||
// Note: Repeater is not thread-safe. Each Repeater instance should not be used
|
||||
// concurrently for different functions. Create separate Repeater instances for
|
||||
// concurrent operations.
|
||||
type Repeater struct {
|
||||
strategy Strategy
|
||||
stats Stats
|
||||
attempts int
|
||||
classifier ErrorClassifier
|
||||
}
|
||||
|
||||
// NewWithStrategy creates a repeater with a custom retry strategy
|
||||
func NewWithStrategy(attempts int, strategy Strategy) *Repeater {
|
||||
if attempts <= 0 {
|
||||
attempts = 1
|
||||
}
|
||||
if strategy == nil {
|
||||
strategy = NewFixedDelay(time.Second)
|
||||
}
|
||||
return &Repeater{
|
||||
attempts: attempts,
|
||||
strategy: strategy,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBackoff creates a repeater with backoff strategy
|
||||
// Default settings (can be overridden with options):
|
||||
// - 30s max delay
|
||||
// - exponential backoff
|
||||
// - 10% jitter
|
||||
func NewBackoff(attempts int, initial time.Duration, opts ...backoffOption) *Repeater {
|
||||
return NewWithStrategy(attempts, newBackoff(initial, opts...))
|
||||
}
|
||||
|
||||
// NewFixed creates a repeater with fixed delay strategy
|
||||
func NewFixed(attempts int, delay time.Duration) *Repeater {
|
||||
return NewWithStrategy(attempts, NewFixedDelay(delay))
|
||||
}
|
||||
|
||||
// Do repeats fun until it succeeds or max attempts reached
|
||||
// terminates immediately on context cancellation or if err matches any in termErrs.
|
||||
// if errs contains ErrAny, terminates on any error.
|
||||
func (r *Repeater) Do(ctx context.Context, fun func() error, termErrs ...error) error {
|
||||
var lastErr error
|
||||
|
||||
// reset and initialize stats
|
||||
r.stats = Stats{
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
// finalizeStats updates the stats before returning
|
||||
finalizeStats := func(attempts int, err error) {
|
||||
r.stats.Attempts = attempts
|
||||
r.stats.LastError = err
|
||||
r.stats.FinishedAt = time.Now()
|
||||
r.stats.TotalDuration = r.stats.FinishedAt.Sub(r.stats.StartedAt)
|
||||
}
|
||||
|
||||
inErrors := func(err error) bool {
|
||||
for _, e := range termErrs {
|
||||
if errors.Is(e, ErrAny) {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for attempt := 0; attempt < r.attempts; attempt++ {
|
||||
// check context before each attempt
|
||||
if err := ctx.Err(); err != nil {
|
||||
finalizeStats(attempt, err)
|
||||
return err //nolint:wrapcheck // context errors are standard and don't need wrapping
|
||||
}
|
||||
|
||||
workStart := time.Now()
|
||||
var err error
|
||||
if err = fun(); err == nil {
|
||||
r.stats.WorkDuration += time.Since(workStart)
|
||||
r.stats.Success = true
|
||||
finalizeStats(attempt+1, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
r.stats.WorkDuration += time.Since(workStart)
|
||||
|
||||
lastErr = err
|
||||
|
||||
// if classifier is set, use it to determine if we should retry
|
||||
if r.classifier != nil {
|
||||
if !r.classifier(err) {
|
||||
finalizeStats(attempt+1, err)
|
||||
return err
|
||||
}
|
||||
} else if inErrors(err) {
|
||||
// fall back to critical errors list if no classifier
|
||||
finalizeStats(attempt+1, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// don't sleep after the last attempt
|
||||
if attempt < r.attempts-1 {
|
||||
delay := r.strategy.NextDelay(attempt + 1)
|
||||
if delay > 0 {
|
||||
delayStart := time.Now()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
r.stats.DelayDuration += time.Since(delayStart)
|
||||
finalizeStats(attempt+1, ctx.Err())
|
||||
return ctx.Err() //nolint:wrapcheck // context errors are standard and don't need wrapping
|
||||
case <-time.After(delay):
|
||||
r.stats.DelayDuration += time.Since(delayStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeStats(r.attempts, lastErr)
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// SetErrorClassifier sets a function to determine if errors are retryable.
|
||||
// This can be used with any repeater strategy (NewFixed, NewBackoff, NewWithStrategy).
|
||||
// When set, the classifier takes precedence over the critical errors list.
|
||||
// Returns true to retry, false to stop immediately.
|
||||
func (r *Repeater) SetErrorClassifier(classifier ErrorClassifier) {
|
||||
r.classifier = classifier
|
||||
}
|
||||
|
||||
// Stats returns the execution statistics from the last Do() call
|
||||
func (r *Repeater) Stats() Stats {
|
||||
return r.stats
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Strategy defines how to calculate delays between retries
|
||||
type Strategy interface {
|
||||
// NextDelay returns delay for the next attempt, attempt starts from 1
|
||||
NextDelay(attempt int) time.Duration
|
||||
}
|
||||
|
||||
// FixedDelay implements fixed time delay between attempts
|
||||
type FixedDelay struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// NewFixedDelay creates a new FixedDelay strategy
|
||||
func NewFixedDelay(delay time.Duration) FixedDelay {
|
||||
return FixedDelay{Delay: delay}
|
||||
}
|
||||
|
||||
// NextDelay returns fixed delay
|
||||
func (s FixedDelay) NextDelay(_ int) time.Duration {
|
||||
return s.Delay
|
||||
}
|
||||
|
||||
// BackoffType represents the backoff strategy type
|
||||
type BackoffType int
|
||||
|
||||
const (
|
||||
// BackoffConstant keeps delays the same between attempts
|
||||
BackoffConstant BackoffType = iota
|
||||
// BackoffLinear increases delays linearly between attempts
|
||||
BackoffLinear
|
||||
// BackoffExponential increases delays exponentially between attempts
|
||||
BackoffExponential
|
||||
)
|
||||
|
||||
// backoff implements various backoff strategies with optional jitter
|
||||
type backoff struct {
|
||||
initial time.Duration
|
||||
maxDelay time.Duration
|
||||
btype BackoffType
|
||||
jitter float64
|
||||
}
|
||||
|
||||
type backoffOption func(*backoff)
|
||||
|
||||
// WithMaxDelay sets maximum delay for the backoff strategy
|
||||
func WithMaxDelay(d time.Duration) backoffOption { //nolint:revive // unexported type is used in the same package
|
||||
return func(b *backoff) {
|
||||
b.maxDelay = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithBackoffType sets backoff type for the strategy
|
||||
func WithBackoffType(t BackoffType) backoffOption { //nolint:revive // unexported type is used in the same package
|
||||
return func(b *backoff) {
|
||||
b.btype = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithJitter sets jitter factor for the backoff strategy
|
||||
func WithJitter(factor float64) backoffOption { //nolint:revive // unexported type is used in the same package
|
||||
return func(b *backoff) {
|
||||
b.jitter = factor
|
||||
}
|
||||
}
|
||||
|
||||
func newBackoff(initial time.Duration, opts ...backoffOption) *backoff {
|
||||
b := &backoff{
|
||||
initial: initial,
|
||||
maxDelay: 30 * time.Second,
|
||||
btype: BackoffExponential,
|
||||
jitter: 0.1,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// NextDelay returns delay for the next attempt
|
||||
func (s backoff) NextDelay(attempt int) time.Duration {
|
||||
if attempt <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var delay time.Duration
|
||||
switch s.btype {
|
||||
case BackoffConstant:
|
||||
delay = s.initial
|
||||
case BackoffLinear:
|
||||
delay = s.initial * time.Duration(attempt)
|
||||
case BackoffExponential:
|
||||
delay = s.initial * time.Duration(1<<(attempt-1))
|
||||
}
|
||||
|
||||
if s.maxDelay > 0 && delay > s.maxDelay {
|
||||
delay = s.maxDelay
|
||||
}
|
||||
|
||||
if s.jitter > 0 {
|
||||
jitter := float64(delay) * s.jitter
|
||||
delay = time.Duration(float64(delay) + (rand.Float64()*jitter - jitter/2)) //nolint:gosec // no need for secure random here
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
||||
+16
-1
@@ -27,6 +27,16 @@ Use the links above for more information on each.
|
||||
|
||||
# changelog
|
||||
|
||||
* Oct 20, 2025 - [1.18.1](https://github.com/klauspost/compress/releases/tag/v1.18.1)
|
||||
* zstd: Add simple zstd EncodeTo/DecodeTo functions https://github.com/klauspost/compress/pull/1079
|
||||
* zstd: Fix incorrect buffer size in dictionary encodes https://github.com/klauspost/compress/pull/1059
|
||||
* s2: check for cap, not len of buffer in EncodeBetter/Best by @vdarulis in https://github.com/klauspost/compress/pull/1080
|
||||
* zlib: Avoiding extra allocation in zlib.reader.Reset by @travelpolicy in https://github.com/klauspost/compress/pull/1086
|
||||
* gzhttp: remove redundant err check in zstdReader by @ryanfowler in https://github.com/klauspost/compress/pull/1090
|
||||
* flate: Faster load+store https://github.com/klauspost/compress/pull/1104
|
||||
* flate: Simplify matchlen https://github.com/klauspost/compress/pull/1101
|
||||
* flate: Use exact sizes for huffman tables https://github.com/klauspost/compress/pull/1103
|
||||
|
||||
* Feb 19th, 2025 - [1.18.0](https://github.com/klauspost/compress/releases/tag/v1.18.0)
|
||||
* Add unsafe little endian loaders https://github.com/klauspost/compress/pull/1036
|
||||
* fix: check `r.err != nil` but return a nil value error `err` by @alingse in https://github.com/klauspost/compress/pull/1028
|
||||
@@ -36,6 +46,9 @@ Use the links above for more information on each.
|
||||
* flate: Fix matchlen L5+L6 https://github.com/klauspost/compress/pull/1049
|
||||
* flate: Cleanup & reduce casts https://github.com/klauspost/compress/pull/1050
|
||||
|
||||
<details>
|
||||
<summary>See changes to v1.17.x</summary>
|
||||
|
||||
* Oct 11th, 2024 - [1.17.11](https://github.com/klauspost/compress/releases/tag/v1.17.11)
|
||||
* zstd: Fix extra CRC written with multiple Close calls https://github.com/klauspost/compress/pull/1017
|
||||
* s2: Don't use stack for index tables https://github.com/klauspost/compress/pull/1014
|
||||
@@ -102,7 +115,8 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
|
||||
* s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
|
||||
* flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
|
||||
* gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
|
||||
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>See changes to v1.16.x</summary>
|
||||
|
||||
@@ -669,3 +683,4 @@ Here are other packages of good quality and pure Go (no cgo wrappers or autoconv
|
||||
# license
|
||||
|
||||
This code is licensed under the same conditions as the original Go code. See LICENSE file.
|
||||
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ func (b *bitWriter) flush32() {
|
||||
// flushAlign will flush remaining full bytes and align to next byte boundary.
|
||||
func (b *bitWriter) flushAlign() {
|
||||
nbBytes := (b.nBits + 7) >> 3
|
||||
for i := uint8(0); i < nbBytes; i++ {
|
||||
for i := range nbBytes {
|
||||
b.out = append(b.out, byte(b.bitContainer>>(i*8)))
|
||||
}
|
||||
b.nBits = 0
|
||||
|
||||
+1
-1
@@ -396,7 +396,7 @@ func (s *Scratch) buildCTable() error {
|
||||
if v > largeLimit {
|
||||
s.zeroBits = true
|
||||
}
|
||||
for nbOccurrences := int16(0); nbOccurrences < v; nbOccurrences++ {
|
||||
for range v {
|
||||
tableSymbol[position] = symbol
|
||||
position = (position + step) & tableMask
|
||||
for position > highThreshold {
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ func (b *bitWriter) flush32() {
|
||||
// flushAlign will flush remaining full bytes and align to next byte boundary.
|
||||
func (b *bitWriter) flushAlign() {
|
||||
nbBytes := (b.nBits + 7) >> 3
|
||||
for i := uint8(0); i < nbBytes; i++ {
|
||||
for i := range nbBytes {
|
||||
b.out = append(b.out, byte(b.bitContainer>>(i*8)))
|
||||
}
|
||||
b.nBits = 0
|
||||
|
||||
+3
-3
@@ -276,7 +276,7 @@ func (s *Scratch) compress4X(src []byte) ([]byte, error) {
|
||||
offsetIdx := len(s.Out)
|
||||
s.Out = append(s.Out, sixZeros[:]...)
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := range 4 {
|
||||
toDo := src
|
||||
if len(toDo) > segmentSize {
|
||||
toDo = toDo[:segmentSize]
|
||||
@@ -312,7 +312,7 @@ func (s *Scratch) compress4Xp(src []byte) ([]byte, error) {
|
||||
segmentSize := (len(src) + 3) / 4
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(4)
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := range 4 {
|
||||
toDo := src
|
||||
if len(toDo) > segmentSize {
|
||||
toDo = toDo[:segmentSize]
|
||||
@@ -326,7 +326,7 @@ func (s *Scratch) compress4Xp(src []byte) ([]byte, error) {
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := range 4 {
|
||||
o := s.tmpOut[i]
|
||||
if len(o) > math.MaxUint16 {
|
||||
// We cannot store the size in the jump table
|
||||
|
||||
+4
-10
@@ -626,7 +626,7 @@ func (d *Decoder) decompress4X8bit(dst, src []byte) ([]byte, error) {
|
||||
|
||||
var br [4]bitReaderBytes
|
||||
start := 6
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
length := int(src[i*2]) | (int(src[i*2+1]) << 8)
|
||||
if start+length >= len(src) {
|
||||
return nil, errors.New("truncated input (or invalid offset)")
|
||||
@@ -798,10 +798,7 @@ func (d *Decoder) decompress4X8bit(dst, src []byte) ([]byte, error) {
|
||||
remainBytes := dstEvery - (decoded / 4)
|
||||
for i := range br {
|
||||
offset := dstEvery * i
|
||||
endsAt := offset + remainBytes
|
||||
if endsAt > len(out) {
|
||||
endsAt = len(out)
|
||||
}
|
||||
endsAt := min(offset+remainBytes, len(out))
|
||||
br := &br[i]
|
||||
bitsLeft := br.remaining()
|
||||
for bitsLeft > 0 {
|
||||
@@ -864,7 +861,7 @@ func (d *Decoder) decompress4X8bit(dst, src []byte) ([]byte, error) {
|
||||
func (d *Decoder) decompress4X8bitExactly(dst, src []byte) ([]byte, error) {
|
||||
var br [4]bitReaderBytes
|
||||
start := 6
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
length := int(src[i*2]) | (int(src[i*2+1]) << 8)
|
||||
if start+length >= len(src) {
|
||||
return nil, errors.New("truncated input (or invalid offset)")
|
||||
@@ -1035,10 +1032,7 @@ func (d *Decoder) decompress4X8bitExactly(dst, src []byte) ([]byte, error) {
|
||||
remainBytes := dstEvery - (decoded / 4)
|
||||
for i := range br {
|
||||
offset := dstEvery * i
|
||||
endsAt := offset + remainBytes
|
||||
if endsAt > len(out) {
|
||||
endsAt = len(out)
|
||||
}
|
||||
endsAt := min(offset+remainBytes, len(out))
|
||||
br := &br[i]
|
||||
bitsLeft := br.remaining()
|
||||
for bitsLeft > 0 {
|
||||
|
||||
+2
-5
@@ -58,7 +58,7 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
|
||||
var br [4]bitReaderShifted
|
||||
// Decode "jump table"
|
||||
start := 6
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
length := int(src[i*2]) | (int(src[i*2+1]) << 8)
|
||||
if start+length >= len(src) {
|
||||
return nil, errors.New("truncated input (or invalid offset)")
|
||||
@@ -109,10 +109,7 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
|
||||
remainBytes := dstEvery - (decoded / 4)
|
||||
for i := range br {
|
||||
offset := dstEvery * i
|
||||
endsAt := offset + remainBytes
|
||||
if endsAt > len(out) {
|
||||
endsAt = len(out)
|
||||
}
|
||||
endsAt := min(offset+remainBytes, len(out))
|
||||
br := &br[i]
|
||||
bitsLeft := br.remaining()
|
||||
for bitsLeft > 0 {
|
||||
|
||||
+2
-2
@@ -201,7 +201,7 @@ func (c cTable) write(s *Scratch) error {
|
||||
for i := range hist[:16] {
|
||||
hist[i] = 0
|
||||
}
|
||||
for n := uint8(0); n < maxSymbolValue; n++ {
|
||||
for n := range maxSymbolValue {
|
||||
v := bitsToWeight[c[n].nBits] & 15
|
||||
huffWeight[n] = v
|
||||
hist[v]++
|
||||
@@ -271,7 +271,7 @@ func (c cTable) estTableSize(s *Scratch) (sz int, err error) {
|
||||
for i := range hist[:16] {
|
||||
hist[i] = 0
|
||||
}
|
||||
for n := uint8(0); n < maxSymbolValue; n++ {
|
||||
for n := range maxSymbolValue {
|
||||
v := bitsToWeight[c[n].nBits] & 15
|
||||
huffWeight[n] = v
|
||||
hist[v]++
|
||||
|
||||
+2
-2
@@ -37,6 +37,6 @@ func Store32(b []byte, v uint32) {
|
||||
}
|
||||
|
||||
// Store64 will store v at b.
|
||||
func Store64(b []byte, v uint64) {
|
||||
binary.LittleEndian.PutUint64(b, v)
|
||||
func Store64[I Indexer](b []byte, i I, v uint64) {
|
||||
binary.LittleEndian.PutUint64(b[i:], v)
|
||||
}
|
||||
|
||||
+3
-6
@@ -38,18 +38,15 @@ func Load64[I Indexer](b []byte, i I) uint64 {
|
||||
|
||||
// Store16 will store v at b.
|
||||
func Store16(b []byte, v uint16) {
|
||||
//binary.LittleEndian.PutUint16(b, v)
|
||||
*(*uint16)(unsafe.Pointer(unsafe.SliceData(b))) = v
|
||||
}
|
||||
|
||||
// Store32 will store v at b.
|
||||
func Store32(b []byte, v uint32) {
|
||||
//binary.LittleEndian.PutUint32(b, v)
|
||||
*(*uint32)(unsafe.Pointer(unsafe.SliceData(b))) = v
|
||||
}
|
||||
|
||||
// Store64 will store v at b.
|
||||
func Store64(b []byte, v uint64) {
|
||||
//binary.LittleEndian.PutUint64(b, v)
|
||||
*(*uint64)(unsafe.Pointer(unsafe.SliceData(b))) = v
|
||||
// Store64 will store v at b[i:].
|
||||
func Store64[I Indexer](b []byte, i I, v uint64) {
|
||||
*(*uint64)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i)) = v
|
||||
}
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ func (r *Reader) fill() error {
|
||||
if !r.readFull(r.buf[:len(magicBody)], false) {
|
||||
return r.err
|
||||
}
|
||||
for i := 0; i < len(magicBody); i++ {
|
||||
for i := range len(magicBody) {
|
||||
if r.buf[i] != magicBody[i] {
|
||||
r.err = ErrCorrupt
|
||||
return r.err
|
||||
|
||||
+3
-1
@@ -20,8 +20,10 @@ import (
|
||||
func Encode(dst, src []byte) []byte {
|
||||
if n := MaxEncodedLen(len(src)); n < 0 {
|
||||
panic(ErrTooLarge)
|
||||
} else if len(dst) < n {
|
||||
} else if cap(dst) < n {
|
||||
dst = make([]byte, n)
|
||||
} else {
|
||||
dst = dst[:n]
|
||||
}
|
||||
|
||||
// The block starts with the varint-encoded length of the decompressed bytes.
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ func (b *bitWriter) flush32() {
|
||||
// flushAlign will flush remaining full bytes and align to next byte boundary.
|
||||
func (b *bitWriter) flushAlign() {
|
||||
nbBytes := (b.nBits + 7) >> 3
|
||||
for i := uint8(0); i < nbBytes; i++ {
|
||||
for i := range nbBytes {
|
||||
b.out = append(b.out, byte(b.bitContainer>>(i*8)))
|
||||
}
|
||||
b.nBits = 0
|
||||
|
||||
+3
-3
@@ -54,11 +54,11 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
huffDecoderPool = sync.Pool{New: func() interface{} {
|
||||
huffDecoderPool = sync.Pool{New: func() any {
|
||||
return &huff0.Scratch{}
|
||||
}}
|
||||
|
||||
fseDecoderPool = sync.Pool{New: func() interface{} {
|
||||
fseDecoderPool = sync.Pool{New: func() any {
|
||||
return &fseDecoder{}
|
||||
}}
|
||||
)
|
||||
@@ -553,7 +553,7 @@ func (b *blockDec) prepareSequences(in []byte, hist *history) (err error) {
|
||||
if compMode&3 != 0 {
|
||||
return errors.New("corrupt block: reserved bits not zero")
|
||||
}
|
||||
for i := uint(0); i < 3; i++ {
|
||||
for i := range uint(3) {
|
||||
mode := seqCompMode((compMode >> (6 - i*2)) & 3)
|
||||
if debugDecoder {
|
||||
println("Table", tableIndex(i), "is", mode)
|
||||
|
||||
+3
-5
@@ -373,11 +373,9 @@ func (d *Decoder) DecodeAll(input, dst []byte) ([]byte, error) {
|
||||
if cap(dst) == 0 && !d.o.limitToCap {
|
||||
// Allocate len(input) * 2 by default if nothing is provided
|
||||
// and we didn't get frame content size.
|
||||
size := len(input) * 2
|
||||
// Cap to 1 MB.
|
||||
if size > 1<<20 {
|
||||
size = 1 << 20
|
||||
}
|
||||
size := min(
|
||||
// Cap to 1 MB.
|
||||
len(input)*2, 1<<20)
|
||||
if uint64(size) > d.o.maxDecodedSize {
|
||||
size = int(d.o.maxDecodedSize)
|
||||
}
|
||||
|
||||
+7
-13
@@ -194,17 +194,17 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
|
||||
hist := o.History
|
||||
contents := o.Contents
|
||||
debug := o.DebugOut != nil
|
||||
println := func(args ...interface{}) {
|
||||
println := func(args ...any) {
|
||||
if o.DebugOut != nil {
|
||||
fmt.Fprintln(o.DebugOut, args...)
|
||||
}
|
||||
}
|
||||
printf := func(s string, args ...interface{}) {
|
||||
printf := func(s string, args ...any) {
|
||||
if o.DebugOut != nil {
|
||||
fmt.Fprintf(o.DebugOut, s, args...)
|
||||
}
|
||||
}
|
||||
print := func(args ...interface{}) {
|
||||
print := func(args ...any) {
|
||||
if o.DebugOut != nil {
|
||||
fmt.Fprint(o.DebugOut, args...)
|
||||
}
|
||||
@@ -424,16 +424,10 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Literal table
|
||||
avgSize := litTotal
|
||||
if avgSize > huff0.BlockSizeMax/2 {
|
||||
avgSize = huff0.BlockSizeMax / 2
|
||||
}
|
||||
avgSize := min(litTotal, huff0.BlockSizeMax/2)
|
||||
huffBuff := make([]byte, 0, avgSize)
|
||||
// Target size
|
||||
div := litTotal / avgSize
|
||||
if div < 1 {
|
||||
div = 1
|
||||
}
|
||||
div := max(litTotal/avgSize, 1)
|
||||
if debug {
|
||||
println("Huffman weights:")
|
||||
}
|
||||
@@ -454,7 +448,7 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
|
||||
huffBuff = append(huffBuff, 255)
|
||||
}
|
||||
scratch := &huff0.Scratch{TableLog: 11}
|
||||
for tries := 0; tries < 255; tries++ {
|
||||
for tries := range 255 {
|
||||
scratch = &huff0.Scratch{TableLog: 11}
|
||||
_, _, err = huff0.Compress1X(huffBuff, scratch)
|
||||
if err == nil {
|
||||
@@ -471,7 +465,7 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
|
||||
|
||||
// Bail out.... Just generate something
|
||||
huffBuff = append(huffBuff, bytes.Repeat([]byte{255}, 10000)...)
|
||||
for i := 0; i < 128; i++ {
|
||||
for i := range 128 {
|
||||
huffBuff = append(huffBuff, byte(i))
|
||||
}
|
||||
continue
|
||||
|
||||
+4
-6
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dictShardBits = 6
|
||||
dictShardBits = 7
|
||||
)
|
||||
|
||||
type fastBase struct {
|
||||
@@ -41,11 +41,9 @@ func (e *fastBase) AppendCRC(dst []byte) []byte {
|
||||
// or a window size small enough to contain the input size, if > 0.
|
||||
func (e *fastBase) WindowSize(size int64) int32 {
|
||||
if size > 0 && size < int64(e.maxMatchOff) {
|
||||
b := int32(1) << uint(bits.Len(uint(size)))
|
||||
// Keep minimum window.
|
||||
if b < 1024 {
|
||||
b = 1024
|
||||
}
|
||||
b := max(
|
||||
// Keep minimum window.
|
||||
int32(1)<<uint(bits.Len(uint(size))), 1024)
|
||||
return b
|
||||
}
|
||||
return e.maxMatchOff
|
||||
|
||||
+6
-17
@@ -158,11 +158,9 @@ func (e *bestFastEncoder) Encode(blk *blockEnc, src []byte) {
|
||||
|
||||
// Use this to estimate literal cost.
|
||||
// Scaled by 10 bits.
|
||||
bitsPerByte := int32((compress.ShannonEntropyBits(src) * 1024) / len(src))
|
||||
// Huffman can never go < 1 bit/byte
|
||||
if bitsPerByte < 1024 {
|
||||
bitsPerByte = 1024
|
||||
}
|
||||
bitsPerByte := max(
|
||||
// Huffman can never go < 1 bit/byte
|
||||
int32((compress.ShannonEntropyBits(src)*1024)/len(src)), 1024)
|
||||
|
||||
// Override src
|
||||
src = e.hist
|
||||
@@ -235,10 +233,7 @@ encodeLoop:
|
||||
// Extend candidate match backwards as far as possible.
|
||||
// Do not extend repeats as we can assume they are optimal
|
||||
// and offsets change if s == nextEmit.
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for offset > tMin && s > nextEmit && src[offset-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
offset--
|
||||
@@ -382,10 +377,7 @@ encodeLoop:
|
||||
nextEmit = s
|
||||
|
||||
// Index skipped...
|
||||
end := s
|
||||
if s > sLimit+4 {
|
||||
end = sLimit + 4
|
||||
}
|
||||
end := min(s, sLimit+4)
|
||||
off := index0 + e.cur
|
||||
for index0 < end {
|
||||
cv0 := load6432(src, index0)
|
||||
@@ -444,10 +436,7 @@ encodeLoop:
|
||||
nextEmit = s
|
||||
|
||||
// Index old s + 1 -> s - 1 or sLimit
|
||||
end := s
|
||||
if s > sLimit-4 {
|
||||
end = sLimit - 4
|
||||
}
|
||||
end := min(s, sLimit-4)
|
||||
|
||||
off := index0 + e.cur
|
||||
for index0 < end {
|
||||
|
||||
+6
-24
@@ -190,10 +190,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch-1 {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -252,10 +249,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch-1 {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -480,10 +474,7 @@ encodeLoop:
|
||||
l := matched
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
t--
|
||||
@@ -719,10 +710,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch-1 {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -783,10 +771,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch-1 {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -1005,10 +990,7 @@ encodeLoop:
|
||||
l := matched
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
t--
|
||||
|
||||
+7
-25
@@ -13,7 +13,7 @@ const (
|
||||
dFastLongLen = 8 // Bytes used for table hash
|
||||
|
||||
dLongTableShardCnt = 1 << (dFastLongTableBits - dictShardBits) // Number of shards in the table
|
||||
dLongTableShardSize = dFastLongTableSize / tableShardCnt // Size of an individual shard
|
||||
dLongTableShardSize = dFastLongTableSize / dLongTableShardCnt // Size of an individual shard
|
||||
|
||||
dFastShortTableBits = tableBits // Bits used in the short match table
|
||||
dFastShortTableSize = 1 << dFastShortTableBits // Size of the table
|
||||
@@ -149,10 +149,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch-1 {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -266,10 +263,7 @@ encodeLoop:
|
||||
l := e.matchlen(s+4, t+4, src) + 4
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
t--
|
||||
@@ -462,10 +456,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -576,10 +567,7 @@ encodeLoop:
|
||||
l := int32(matchLen(src[s+4:], src[t+4:])) + 4
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] {
|
||||
s--
|
||||
t--
|
||||
@@ -809,10 +797,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > tMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch-1 {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -927,10 +912,7 @@ encodeLoop:
|
||||
l := e.matchlen(s+4, t+4, src) + 4
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
t--
|
||||
|
||||
+6
-24
@@ -143,10 +143,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
sMin := s - e.maxMatchOff
|
||||
if sMin < 0 {
|
||||
sMin = 0
|
||||
}
|
||||
sMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > sMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -223,10 +220,7 @@ encodeLoop:
|
||||
l := e.matchlen(s+4, t+4, src) + 4
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
t--
|
||||
@@ -387,10 +381,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
sMin := s - e.maxMatchOff
|
||||
if sMin < 0 {
|
||||
sMin = 0
|
||||
}
|
||||
sMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > sMin && start > startLimit && src[repIndex-1] == src[start-1] {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -469,10 +460,7 @@ encodeLoop:
|
||||
l := e.matchlen(s+4, t+4, src) + 4
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] {
|
||||
s--
|
||||
t--
|
||||
@@ -655,10 +643,7 @@ encodeLoop:
|
||||
// and have to do special offset treatment.
|
||||
startLimit := nextEmit + 1
|
||||
|
||||
sMin := s - e.maxMatchOff
|
||||
if sMin < 0 {
|
||||
sMin = 0
|
||||
}
|
||||
sMin := max(s-e.maxMatchOff, 0)
|
||||
for repIndex > sMin && start > startLimit && src[repIndex-1] == src[start-1] && seq.matchLen < maxMatchLength-zstdMinMatch {
|
||||
repIndex--
|
||||
start--
|
||||
@@ -735,10 +720,7 @@ encodeLoop:
|
||||
l := e.matchlen(s+4, t+4, src) + 4
|
||||
|
||||
// Extend backwards
|
||||
tMin := s - e.maxMatchOff
|
||||
if tMin < 0 {
|
||||
tMin = 0
|
||||
}
|
||||
tMin := max(s-e.maxMatchOff, 0)
|
||||
for t > tMin && s > nextEmit && src[t-1] == src[s-1] && l < maxMatchLength {
|
||||
s--
|
||||
t--
|
||||
|
||||
+1
-4
@@ -238,10 +238,7 @@ func (d *frameDec) reset(br byteBuffer) error {
|
||||
|
||||
if d.WindowSize == 0 && d.SingleSegment {
|
||||
// We may not need window in this case.
|
||||
d.WindowSize = d.FrameContentSize
|
||||
if d.WindowSize < MinWindowSize {
|
||||
d.WindowSize = MinWindowSize
|
||||
}
|
||||
d.WindowSize = max(d.FrameContentSize, MinWindowSize)
|
||||
if d.WindowSize > d.o.maxDecodedSize {
|
||||
if debugDecoder {
|
||||
printf("window size %d > max %d\n", d.WindowSize, d.o.maxWindowSize)
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ func (s *fseEncoder) buildCTable() error {
|
||||
if v > largeLimit {
|
||||
s.zeroBits = true
|
||||
}
|
||||
for nbOccurrences := int16(0); nbOccurrences < v; nbOccurrences++ {
|
||||
for range v {
|
||||
tableSymbol[position] = symbol
|
||||
position = (position + step) & tableMask
|
||||
for position > highThreshold {
|
||||
|
||||
+1
-4
@@ -231,10 +231,7 @@ func (s *sequenceDecs) decodeSync(hist []byte) error {
|
||||
llTable, mlTable, ofTable := s.litLengths.fse.dt[:maxTablesize], s.matchLengths.fse.dt[:maxTablesize], s.offsets.fse.dt[:maxTablesize]
|
||||
llState, mlState, ofState := s.litLengths.state.state, s.matchLengths.state.state, s.offsets.state.state
|
||||
out := s.out
|
||||
maxBlockSize := maxCompressedBlockSize
|
||||
if s.windowSize < maxBlockSize {
|
||||
maxBlockSize = s.windowSize
|
||||
}
|
||||
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
|
||||
|
||||
if debugDecoder {
|
||||
println("decodeSync: decoding", seqs, "sequences", br.remain(), "bits remain on stream")
|
||||
|
||||
+2
-8
@@ -79,10 +79,7 @@ func (s *sequenceDecs) decodeSyncSimple(hist []byte) (bool, error) {
|
||||
|
||||
br := s.br
|
||||
|
||||
maxBlockSize := maxCompressedBlockSize
|
||||
if s.windowSize < maxBlockSize {
|
||||
maxBlockSize = s.windowSize
|
||||
}
|
||||
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
|
||||
|
||||
ctx := decodeSyncAsmContext{
|
||||
llTable: s.litLengths.fse.dt[:maxTablesize],
|
||||
@@ -237,10 +234,7 @@ func sequenceDecs_decode_56_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmC
|
||||
func (s *sequenceDecs) decode(seqs []seqVals) error {
|
||||
br := s.br
|
||||
|
||||
maxBlockSize := maxCompressedBlockSize
|
||||
if s.windowSize < maxBlockSize {
|
||||
maxBlockSize = s.windowSize
|
||||
}
|
||||
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
|
||||
|
||||
ctx := decodeAsmContext{
|
||||
llTable: s.litLengths.fse.dt[:maxTablesize],
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2025+ Klaus Post. All rights reserved.
|
||||
// License information can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.24
|
||||
|
||||
package zstd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"sync"
|
||||
"weak"
|
||||
)
|
||||
|
||||
var weakMu sync.Mutex
|
||||
var simpleEnc weak.Pointer[Encoder]
|
||||
var simpleDec weak.Pointer[Decoder]
|
||||
|
||||
// EncodeTo appends the encoded data from src to dst.
|
||||
func EncodeTo(dst []byte, src []byte) []byte {
|
||||
weakMu.Lock()
|
||||
enc := simpleEnc.Value()
|
||||
if enc == nil {
|
||||
var err error
|
||||
enc, err = NewWriter(nil, WithEncoderConcurrency(runtime.NumCPU()), WithWindowSize(1<<20), WithLowerEncoderMem(true), WithZeroFrames(true))
|
||||
if err != nil {
|
||||
panic("failed to create simple encoder: " + err.Error())
|
||||
}
|
||||
simpleEnc = weak.Make(enc)
|
||||
}
|
||||
weakMu.Unlock()
|
||||
|
||||
return enc.EncodeAll(src, dst)
|
||||
}
|
||||
|
||||
// DecodeTo appends the decoded data from src to dst.
|
||||
// The maximum decoded size is 1GiB,
|
||||
// not including what may already be in dst.
|
||||
func DecodeTo(dst []byte, src []byte) ([]byte, error) {
|
||||
weakMu.Lock()
|
||||
dec := simpleDec.Value()
|
||||
if dec == nil {
|
||||
var err error
|
||||
dec, err = NewReader(nil, WithDecoderConcurrency(runtime.NumCPU()), WithDecoderLowmem(true), WithDecoderMaxMemory(1<<30))
|
||||
if err != nil {
|
||||
weakMu.Unlock()
|
||||
return nil, errors.New("failed to create simple decoder: " + err.Error())
|
||||
}
|
||||
runtime.SetFinalizer(dec, func(d *Decoder) {
|
||||
d.Close()
|
||||
})
|
||||
simpleDec = weak.Make(dec)
|
||||
}
|
||||
weakMu.Unlock()
|
||||
return dec.DecodeAll(src, dst)
|
||||
}
|
||||
+1
-1
@@ -257,7 +257,7 @@ func (r *SnappyConverter) Convert(in io.Reader, w io.Writer) (int64, error) {
|
||||
if !r.readFull(r.buf[:len(snappyMagicBody)], false) {
|
||||
return written, r.err
|
||||
}
|
||||
for i := 0; i < len(snappyMagicBody); i++ {
|
||||
for i := range len(snappyMagicBody) {
|
||||
if r.buf[i] != snappyMagicBody[i] {
|
||||
println("r.buf[i] != snappyMagicBody[i]", r.buf[i], snappyMagicBody[i], i)
|
||||
r.err = ErrSnappyCorrupt
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ const ZipMethodWinZip = 93
|
||||
const ZipMethodPKWare = 20
|
||||
|
||||
// zipReaderPool is the default reader pool.
|
||||
var zipReaderPool = sync.Pool{New: func() interface{} {
|
||||
var zipReaderPool = sync.Pool{New: func() any {
|
||||
z, err := NewReader(nil, WithDecoderLowmem(true), WithDecoderMaxWindow(128<<20), WithDecoderConcurrency(1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
+2
-2
@@ -98,13 +98,13 @@ var (
|
||||
ErrDecoderNilInput = errors.New("nil input provided as reader")
|
||||
)
|
||||
|
||||
func println(a ...interface{}) {
|
||||
func println(a ...any) {
|
||||
if debug || debugDecoder || debugEncoder {
|
||||
log.Println(a...)
|
||||
}
|
||||
}
|
||||
|
||||
func printf(format string, a ...interface{}) {
|
||||
func printf(format string, a ...any) {
|
||||
if debug || debugDecoder || debugEncoder {
|
||||
log.Printf(format, a...)
|
||||
}
|
||||
|
||||
+10
-1
@@ -3,4 +3,13 @@ testdata/*
|
||||
.idea/
|
||||
.DS_Store
|
||||
*.tar.gz
|
||||
*.dic
|
||||
*.dic
|
||||
redis8tests.sh
|
||||
coverage.txt
|
||||
**/coverage.txt
|
||||
.vscode
|
||||
tmp/*
|
||||
*.test
|
||||
|
||||
# maintenanceNotifications upgrade documentation (temporary)
|
||||
maintenanceNotifications/docs/
|
||||
|
||||
+31
@@ -1,3 +1,34 @@
|
||||
version: "2"
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: false
|
||||
linters:
|
||||
settings:
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
# Incorrect or missing package comment.
|
||||
# https://staticcheck.dev/docs/checks/#ST1000
|
||||
- -ST1000
|
||||
# Omit embedded fields from selector expression.
|
||||
# https://staticcheck.dev/docs/checks/#QF1008
|
||||
- -QF1008
|
||||
- -ST1003
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980))
|
||||
Users can use the OpenTelemetry sampler to control the sampling behavior.
|
||||
For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep
|
||||
a similar behavior (drop orphan spans) of `go-redis` as before.
|
||||
|
||||
## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602))
|
||||
* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe))
|
||||
* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af))
|
||||
|
||||
|
||||
|
||||
## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e))
|
||||
* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8))
|
||||
* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af))
|
||||
|
||||
|
||||
|
||||
## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02)
|
||||
|
||||
### New Features
|
||||
|
||||
- feat(scan): scan time.Time sets the default decoding (#2413)
|
||||
- Add support for CLUSTER LINKS command (#2504)
|
||||
- Add support for acl dryrun command (#2502)
|
||||
- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500)
|
||||
- Add support for LCS Command (#2480)
|
||||
- Add support for BZMPOP (#2456)
|
||||
- Adding support for ZMPOP command (#2408)
|
||||
- Add support for LMPOP (#2440)
|
||||
- feat: remove pool unused fields (#2438)
|
||||
- Expiretime and PExpireTime (#2426)
|
||||
- Implement `FUNCTION` group of commands (#2475)
|
||||
- feat(zadd): add ZAddLT and ZAddGT (#2429)
|
||||
- Add: Support for COMMAND LIST command (#2491)
|
||||
- Add support for BLMPOP (#2442)
|
||||
- feat: check pipeline.Do to prevent confusion with Exec (#2517)
|
||||
- Function stats, function kill, fcall and fcall_ro (#2486)
|
||||
- feat: Add support for CLUSTER SHARDS command (#2507)
|
||||
- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498)
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix: eval api cmd.SetFirstKeyPos (#2501)
|
||||
- fix: limit the number of connections created (#2441)
|
||||
- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479)
|
||||
- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458)
|
||||
- fix: group lag can be null (#2448)
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Updating to the latest version of redis (#2508)
|
||||
- Allowing for running tests on a port other than the fixed 6380 (#2466)
|
||||
- redis 7.0.8 in tests (#2450)
|
||||
- docs: Update redisotel example for v9 (#2425)
|
||||
- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476)
|
||||
- chore: add Chinese translation (#2436)
|
||||
- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421)
|
||||
- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420)
|
||||
- chore(deps): bump actions/setup-go from 3 to 4 (#2495)
|
||||
- docs: add instructions for the HSet api (#2503)
|
||||
- docs: add reading lag field comment (#2451)
|
||||
- test: update go mod before testing(go mod tidy) (#2423)
|
||||
- docs: fix comment typo (#2505)
|
||||
- test: remove testify (#2463)
|
||||
- refactor: change ListElementCmd to KeyValuesCmd. (#2443)
|
||||
- fix(appendArg): appendArg case special type (#2489)
|
||||
|
||||
## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01)
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65))
|
||||
|
||||
## v9 2023-01-30
|
||||
|
||||
### Breaking
|
||||
|
||||
- Changed Pipelines to not be thread-safe any more.
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was
|
||||
contributed by @monkey92t who has done the majority of work in this release.
|
||||
- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts
|
||||
and deadlines. See
|
||||
[Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details.
|
||||
- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example,
|
||||
`redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`.
|
||||
- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See
|
||||
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html)
|
||||
- Added `redis.HasErrorPrefix` to help working with errors.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is
|
||||
completely gone in v9.
|
||||
- Reworked hook interface and added `DialHook`.
|
||||
- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See
|
||||
[example](example/otel) and
|
||||
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html).
|
||||
- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making
|
||||
an allocation.
|
||||
- Renamed the option `MaxConnAge` to `ConnMaxLifetime`.
|
||||
- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`.
|
||||
- Removed connection reaper in favor of `MaxIdleConns`.
|
||||
- Removed `WithContext` since `context.Context` can be passed directly as an arg.
|
||||
- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and
|
||||
it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to
|
||||
reset commands for some reason.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved and fixed pipeline retries.
|
||||
- As usually, added support for more commands and fixed some bugs.
|
||||
+22
-5
@@ -32,20 +32,33 @@ Here's how to get started with your code contribution:
|
||||
|
||||
1. Create your own fork of go-redis
|
||||
2. Do the changes in your fork
|
||||
3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running:
|
||||
```docker run -p 6379:6379 -it redis/redis-stack-server:edge```
|
||||
4. While developing, make sure the tests pass by running `make tests`
|
||||
3. If you need a development environment, run `make docker.start`.
|
||||
|
||||
> Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about
|
||||
> the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity
|
||||
> to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`.
|
||||
> By default the docker image that will be pulled and started is `redislabs/client-libs-test:8.2.1-pre`.
|
||||
> If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box.
|
||||
|
||||
4. While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient).
|
||||
> Note: `make test` will try to start all containers, run the tests with `make test.ci` and then stop all containers.
|
||||
5. If you like the change and think the project could use it, send a
|
||||
pull request
|
||||
|
||||
To see what else is part of the automation, run `invoke -l`
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Call `make test` to run all tests, including linters.
|
||||
### Setting up Docker
|
||||
To run the tests, you need to have Docker installed and running. If you are using a host OS that does not support
|
||||
docker host networks out of the box (e.g. Windows, OSX), you need to set up a docker desktop and enable docker host networks.
|
||||
|
||||
### Running tests
|
||||
Call `make test` to run all tests.
|
||||
|
||||
Continuous Integration uses these same wrappers to run all of these
|
||||
tests against multiple versions of python. Feel free to test your
|
||||
tests against multiple versions of redis. Feel free to test your
|
||||
changes against all the go versions supported, as declared by the
|
||||
[build.yml](./.github/workflows/build.yml) file.
|
||||
|
||||
@@ -99,3 +112,7 @@ The core team regularly looks at pull requests. We will provide
|
||||
feedback as soon as possible. After receiving our feedback, please respond
|
||||
within two weeks. After that time, we may close your PR if it isn't
|
||||
showing any activity.
|
||||
|
||||
## Support
|
||||
|
||||
Maintainers can provide limited support to contributors on discord: https://discord.gg/W4txy5AeKM
|
||||
|
||||
+60
-23
@@ -1,42 +1,79 @@
|
||||
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
|
||||
REDIS_VERSION ?= 8.4
|
||||
RE_CLUSTER ?= false
|
||||
RCE_DOCKER ?= true
|
||||
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.4.0
|
||||
|
||||
test: testdeps
|
||||
$(eval GO_VERSION := $(shell go version | cut -d " " -f 3 | cut -d. -f2))
|
||||
docker.start:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
export CLIENT_LIBS_TEST_IMAGE=$(CLIENT_LIBS_TEST_IMAGE) && \
|
||||
docker compose --profile all up -d --quiet-pull
|
||||
|
||||
docker.stop:
|
||||
docker compose --profile all down
|
||||
|
||||
test:
|
||||
$(MAKE) docker.start
|
||||
@if [ -z "$(REDIS_VERSION)" ]; then \
|
||||
echo "REDIS_VERSION not set, running all tests"; \
|
||||
$(MAKE) test.ci; \
|
||||
else \
|
||||
MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \
|
||||
if [ "$$MAJOR_VERSION" -ge 8 ]; then \
|
||||
echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \
|
||||
$(MAKE) test.ci; \
|
||||
else \
|
||||
echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \
|
||||
$(MAKE) test.ci.skip-vectorsets; \
|
||||
fi; \
|
||||
fi
|
||||
$(MAKE) docker.stop
|
||||
|
||||
test.ci:
|
||||
set -e; for dir in $(GO_MOD_DIRS); do \
|
||||
if echo "$${dir}" | grep -q "./example" && [ "$(GO_VERSION)" = "19" ]; then \
|
||||
echo "Skipping go test in $${dir} due to Go version 1.19 and dir contains ./example"; \
|
||||
continue; \
|
||||
fi; \
|
||||
echo "go test in $${dir}"; \
|
||||
(cd "$${dir}" && \
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go mod tidy -compat=1.18 && \
|
||||
go test && \
|
||||
go test ./... -short -race && \
|
||||
go test ./... -run=NONE -bench=. -benchmem && \
|
||||
env GOOS=linux GOARCH=386 go test && \
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./... && \
|
||||
go vet); \
|
||||
go vet && \
|
||||
go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \
|
||||
done
|
||||
cd internal/customvet && go build .
|
||||
go vet -vettool ./internal/customvet/customvet
|
||||
|
||||
testdeps: testdata/redis/src/redis-server
|
||||
test.ci.skip-vectorsets:
|
||||
set -e; for dir in $(GO_MOD_DIRS); do \
|
||||
echo "go test in $${dir} (skipping vector sets)"; \
|
||||
(cd "$${dir}" && \
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go mod tidy -compat=1.18 && \
|
||||
go vet && \
|
||||
go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \
|
||||
-run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \
|
||||
done
|
||||
cd internal/customvet && go build .
|
||||
go vet -vettool ./internal/customvet/customvet
|
||||
|
||||
bench: testdeps
|
||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem
|
||||
bench:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example
|
||||
|
||||
.PHONY: all test testdeps bench fmt
|
||||
.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt
|
||||
|
||||
build:
|
||||
export RE_CLUSTER=$(RE_CLUSTER) && \
|
||||
export RCE_DOCKER=$(RCE_DOCKER) && \
|
||||
export REDIS_VERSION=$(REDIS_VERSION) && \
|
||||
go build .
|
||||
|
||||
testdata/redis:
|
||||
mkdir -p $@
|
||||
wget -qO- https://download.redis.io/releases/redis-7.4-rc2.tar.gz | tar xvz --strip-components=1 -C $@
|
||||
|
||||
testdata/redis/src/redis-server: testdata/redis
|
||||
cd $< && make all
|
||||
|
||||
fmt:
|
||||
gofumpt -w ./
|
||||
goimports -w -local github.com/redis/go-redis ./
|
||||
|
||||
+353
-49
@@ -2,17 +2,32 @@
|
||||
|
||||
[](https://github.com/redis/go-redis/actions)
|
||||
[](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
|
||||
[](https://redis.uptrace.dev/)
|
||||
[](https://redis.io/docs/latest/develop/clients/go/)
|
||||
[](https://goreportcard.com/report/github.com/redis/go-redis/v9)
|
||||
[](https://codecov.io/github/redis/go-redis)
|
||||
[](https://discord.gg/rWtp5Aj)
|
||||
|
||||
> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
|
||||
> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can
|
||||
> use it to monitor applications and set up automatic alerts to receive notifications via email,
|
||||
> Slack, Telegram, and others.
|
||||
>
|
||||
> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which
|
||||
> demonstrates how you can use Uptrace to monitor go-redis.
|
||||
[](https://discord.gg/W4txy5AeKM)
|
||||
[](https://www.twitch.tv/redisinc)
|
||||
[](https://www.youtube.com/redisinc)
|
||||
[](https://twitter.com/redisinc)
|
||||
[](https://stackoverflow.com/questions/tagged/go-redis)
|
||||
|
||||
> go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers.
|
||||
|
||||
## Supported versions
|
||||
|
||||
In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support:
|
||||
- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0
|
||||
- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2
|
||||
- [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4
|
||||
|
||||
Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three
|
||||
versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0),
|
||||
[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with
|
||||
Redis Stack 7.2 and some commands are changed with Redis CE 8.0.
|
||||
Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+.
|
||||
Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version
|
||||
in the `go.mod` to `go 1.24` in one of the next releases.
|
||||
|
||||
## How do I Redis?
|
||||
|
||||
@@ -28,40 +43,39 @@
|
||||
|
||||
[Work at Redis](https://redis.com/company/careers/jobs/)
|
||||
|
||||
## Documentation
|
||||
|
||||
- [English](https://redis.uptrace.dev)
|
||||
- [简体中文](https://redis.uptrace.dev/zh/)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Discussions](https://github.com/redis/go-redis/discussions)
|
||||
- [Chat](https://discord.gg/rWtp5Aj)
|
||||
- [Chat](https://discord.gg/W4txy5AeKM)
|
||||
- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9)
|
||||
- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples)
|
||||
|
||||
## old documentation
|
||||
|
||||
- [English](https://redis.uptrace.dev)
|
||||
- [简体中文](https://redis.uptrace.dev/zh/)
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [Redis Mock](https://github.com/go-redis/redismock)
|
||||
- [Entra ID (Azure AD)](https://github.com/redis/go-redis-entraid)
|
||||
- [Distributed Locks](https://github.com/bsm/redislock)
|
||||
- [Redis Cache](https://github.com/go-redis/cache)
|
||||
- [Rate limiting](https://github.com/go-redis/redis_rate)
|
||||
|
||||
This client also works with [Kvrocks](https://github.com/apache/incubator-kvrocks), a distributed
|
||||
key value NoSQL database that uses RocksDB as storage engine and is compatible with Redis protocol.
|
||||
|
||||
## Features
|
||||
|
||||
- Redis commands except QUIT and SYNC.
|
||||
- Automatic connection pooling.
|
||||
- [StreamingCredentialsProvider (e.g. entra id, oauth)](#1-streaming-credentials-provider-highest-priority) (experimental)
|
||||
- [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html).
|
||||
- [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html).
|
||||
- [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html).
|
||||
- [Redis Sentinel](https://redis.uptrace.dev/guide/go-redis-sentinel.html).
|
||||
- [Redis Cluster](https://redis.uptrace.dev/guide/go-redis-cluster.html).
|
||||
- [Redis Ring](https://redis.uptrace.dev/guide/ring.html).
|
||||
- [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html).
|
||||
- [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/)
|
||||
- [Customizable read and write buffers size.](#custom-buffer-sizes)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -122,17 +136,121 @@ func ExampleClient() {
|
||||
}
|
||||
```
|
||||
|
||||
The above can be modified to specify the version of the RESP protocol by adding the `protocol`
|
||||
option to the `Options` struct:
|
||||
### Authentication
|
||||
|
||||
The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options:
|
||||
|
||||
#### 1. Streaming Credentials Provider (Highest Priority) - Experimental feature
|
||||
|
||||
The streaming credentials provider allows for dynamic credential updates during the connection lifetime. This is particularly useful for managed identity services and token-based authentication.
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3
|
||||
})
|
||||
type StreamingCredentialsProvider interface {
|
||||
Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error)
|
||||
}
|
||||
|
||||
type CredentialsListener interface {
|
||||
OnNext(credentials Credentials) // Called when credentials are updated
|
||||
OnError(err error) // Called when an error occurs
|
||||
}
|
||||
|
||||
type Credentials interface {
|
||||
BasicAuth() (username string, password string)
|
||||
RawCredentials() string
|
||||
}
|
||||
```
|
||||
|
||||
Example usage:
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
StreamingCredentialsProvider: &MyCredentialsProvider{},
|
||||
})
|
||||
```
|
||||
|
||||
**Note:** The streaming credentials provider can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) to enable Entra ID (formerly Azure AD) authentication. This allows for seamless integration with Azure's managed identity services and token-based authentication.
|
||||
|
||||
Example with Entra ID:
|
||||
```go
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis-entraid"
|
||||
)
|
||||
|
||||
// Create an Entra ID credentials provider
|
||||
provider := entraid.NewDefaultAzureIdentityProvider()
|
||||
|
||||
// Configure Redis client with Entra ID authentication
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "your-redis-server.redis.cache.windows.net:6380",
|
||||
StreamingCredentialsProvider: provider,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### 2. Context-based Credentials Provider
|
||||
|
||||
The context-based provider allows credentials to be determined at the time of each operation, using the context.
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
CredentialsProviderContext: func(ctx context.Context) (string, string, error) {
|
||||
// Return username, password, and any error
|
||||
return "user", "pass", nil
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. Regular Credentials Provider
|
||||
|
||||
A simple function-based provider that returns static credentials.
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
CredentialsProvider: func() (string, string) {
|
||||
// Return username and password
|
||||
return "user", "pass"
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### 4. Username/Password Fields (Lowest Priority)
|
||||
|
||||
The most basic way to provide credentials is through the `Username` and `Password` fields in the options.
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
})
|
||||
```
|
||||
|
||||
#### Priority Order
|
||||
|
||||
The client will use credentials in the following priority order:
|
||||
1. Streaming Credentials Provider (if set)
|
||||
2. Context-based Credentials Provider (if set)
|
||||
3. Regular Credentials Provider (if set)
|
||||
4. Username/Password fields (if set)
|
||||
|
||||
If none of these are set, the client will attempt to connect without authentication.
|
||||
|
||||
### Protocol Version
|
||||
|
||||
The client supports both RESP2 and RESP3 protocols. You can specify the protocol version in the options:
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3
|
||||
})
|
||||
```
|
||||
|
||||
### Connecting via a redis url
|
||||
@@ -159,6 +277,36 @@ func ExampleClient() *redis.Client {
|
||||
|
||||
```
|
||||
|
||||
### Instrument with OpenTelemetry
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/extra/redisotel/v9"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
...
|
||||
rdb := redis.NewClient(&redis.Options{...})
|
||||
|
||||
if err := errors.Join(redisotel.InstrumentTracing(rdb), redisotel.InstrumentMetrics(rdb)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Buffer Size Configuration
|
||||
|
||||
go-redis uses 32KiB read and write buffers by default for optimal performance. For high-throughput applications or large pipelines, you can customize buffer sizes:
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
ReadBufferSize: 1024 * 1024, // 1MiB read buffer
|
||||
WriteBufferSize: 1024 * 1024, // 1MiB write buffer
|
||||
})
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
@@ -203,9 +351,45 @@ res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptio
|
||||
val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal()
|
||||
```
|
||||
|
||||
## Contributing
|
||||
#### Redis-Search Default Dialect
|
||||
|
||||
Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library!
|
||||
In the Redis-Search module, **the default dialect is 2**. If needed, you can explicitly specify a different dialect using the appropriate configuration in your queries.
|
||||
|
||||
**Important**: Be aware that the query dialect may impact the results returned. If needed, you can revert to a different dialect version by passing the desired dialect in the arguments of the command you want to execute.
|
||||
For example:
|
||||
```
|
||||
res2, err := rdb.FTSearchWithArgs(ctx,
|
||||
"idx:bicycle",
|
||||
"@pickup_zone:[CONTAINS $bike]",
|
||||
&redis.FTSearchOptions{
|
||||
Params: map[string]interface{}{
|
||||
"bike": "POINT(-0.1278 51.5074)",
|
||||
},
|
||||
DialectVersion: 3,
|
||||
},
|
||||
).Result()
|
||||
```
|
||||
You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/).
|
||||
|
||||
#### Custom buffer sizes
|
||||
Prior to v9.12, the buffer size was the default go value of 4096 bytes. Starting from v9.12,
|
||||
go-redis uses 32KiB read and write buffers by default for optimal performance.
|
||||
For high-throughput applications or large pipelines, you can customize buffer sizes:
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
ReadBufferSize: 1024 * 1024, // 1MiB read buffer
|
||||
WriteBufferSize: 1024 * 1024, // 1MiB write buffer
|
||||
})
|
||||
```
|
||||
|
||||
**Important**: If you experience any issues with the default buffer sizes, please try setting them to the go default of 4096 bytes.
|
||||
|
||||
## Contributing
|
||||
We welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub.
|
||||
We appreciate your help in making go-redis better for everyone.
|
||||
If you are interested in contributing to the go-redis library, please check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to get started.
|
||||
|
||||
## Look and feel
|
||||
|
||||
@@ -242,38 +426,150 @@ vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello")
|
||||
res, err := rdb.Do(ctx, "set", "key", "value").Result()
|
||||
```
|
||||
|
||||
## Run the test
|
||||
## Typed Errors
|
||||
|
||||
go-redis will start a redis-server and run the test cases.
|
||||
|
||||
The paths of redis-server bin file and redis config file are defined in `main_test.go`:
|
||||
go-redis provides typed error checking functions for common Redis errors:
|
||||
|
||||
```go
|
||||
var (
|
||||
redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server"))
|
||||
redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf"))
|
||||
)
|
||||
// Cluster and replication errors
|
||||
redis.IsLoadingError(err) // Redis is loading the dataset
|
||||
redis.IsReadOnlyError(err) // Write to read-only replica
|
||||
redis.IsClusterDownError(err) // Cluster is down
|
||||
redis.IsTryAgainError(err) // Command should be retried
|
||||
redis.IsMasterDownError(err) // Master is down
|
||||
redis.IsMovedError(err) // Returns (address, true) if key moved
|
||||
redis.IsAskError(err) // Returns (address, true) if key being migrated
|
||||
|
||||
// Connection and resource errors
|
||||
redis.IsMaxClientsError(err) // Maximum clients reached
|
||||
redis.IsAuthError(err) // Authentication failed (NOAUTH, WRONGPASS, unauthenticated)
|
||||
redis.IsPermissionError(err) // Permission denied (NOPERM)
|
||||
redis.IsOOMError(err) // Out of memory (OOM)
|
||||
|
||||
// Transaction errors
|
||||
redis.IsExecAbortError(err) // Transaction aborted (EXECABORT)
|
||||
```
|
||||
|
||||
For local testing, you can change the variables to refer to your local files, or create a soft link
|
||||
to the corresponding folder for redis-server and copy the config file to `testdata/redis/`:
|
||||
### Error Wrapping in Hooks
|
||||
|
||||
```shell
|
||||
ln -s /usr/bin/redis-server ./go-redis/testdata/redis/src
|
||||
cp ./go-redis/testdata/redis.conf ./go-redis/testdata/redis/
|
||||
When wrapping errors in hooks, use custom error types with `Unwrap()` method (preferred) or `fmt.Errorf` with `%w`. Always call `cmd.SetErr()` to preserve error type information:
|
||||
|
||||
```go
|
||||
// Custom error type (preferred)
|
||||
type AppError struct {
|
||||
Code string
|
||||
RequestID string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
return fmt.Sprintf("[%s] request_id=%s: %v", e.Code, e.RequestID, e.Err)
|
||||
}
|
||||
|
||||
func (e *AppError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Hook implementation
|
||||
func (h MyHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||
err := next(ctx, cmd)
|
||||
if err != nil {
|
||||
// Wrap with custom error type
|
||||
wrappedErr := &AppError{
|
||||
Code: "REDIS_ERROR",
|
||||
RequestID: getRequestID(ctx),
|
||||
Err: err,
|
||||
}
|
||||
cmd.SetErr(wrappedErr)
|
||||
return wrappedErr // Return wrapped error to preserve it
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Typed error detection works through wrappers
|
||||
if redis.IsLoadingError(err) {
|
||||
// Retry logic
|
||||
}
|
||||
|
||||
// Extract custom error if needed
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) {
|
||||
log.Printf("Request: %s", appErr.RequestID)
|
||||
}
|
||||
```
|
||||
|
||||
Lastly, run:
|
||||
|
||||
```shell
|
||||
go test
|
||||
Alternatively, use `fmt.Errorf` with `%w`:
|
||||
```go
|
||||
wrappedErr := fmt.Errorf("context: %w", err)
|
||||
cmd.SetErr(wrappedErr)
|
||||
```
|
||||
|
||||
Another option is to run your specific tests with an already running redis. The example below, tests
|
||||
against a redis running on port 9999.:
|
||||
### Pipeline Hook Example
|
||||
|
||||
For pipeline operations, use `ProcessPipelineHook`:
|
||||
|
||||
```go
|
||||
type PipelineLoggingHook struct{}
|
||||
|
||||
func (h PipelineLoggingHook) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return next
|
||||
}
|
||||
|
||||
func (h PipelineLoggingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return next
|
||||
}
|
||||
|
||||
func (h PipelineLoggingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
|
||||
return func(ctx context.Context, cmds []redis.Cmder) error {
|
||||
start := time.Now()
|
||||
|
||||
// Execute the pipeline
|
||||
err := next(ctx, cmds)
|
||||
|
||||
duration := time.Since(start)
|
||||
log.Printf("Pipeline executed %d commands in %v", len(cmds), duration)
|
||||
|
||||
// Process individual command errors
|
||||
// Note: Individual command errors are already set on each cmd by the pipeline execution
|
||||
for _, cmd := range cmds {
|
||||
if cmdErr := cmd.Err(); cmdErr != nil {
|
||||
// Check for specific error types using typed error functions
|
||||
if redis.IsAuthError(cmdErr) {
|
||||
log.Printf("Auth error in pipeline command %s: %v", cmd.Name(), cmdErr)
|
||||
} else if redis.IsPermissionError(cmdErr) {
|
||||
log.Printf("Permission error in pipeline command %s: %v", cmd.Name(), cmdErr)
|
||||
}
|
||||
|
||||
// Optionally wrap individual command errors to add context
|
||||
// The wrapped error preserves type information through errors.As()
|
||||
wrappedErr := fmt.Errorf("pipeline cmd %s failed: %w", cmd.Name(), cmdErr)
|
||||
cmd.SetErr(wrappedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the pipeline-level error (connection errors, etc.)
|
||||
// You can wrap it if needed, or return it as-is
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Register the hook
|
||||
rdb.AddHook(PipelineLoggingHook{})
|
||||
|
||||
// Use pipeline - errors are still properly typed
|
||||
pipe := rdb.Pipeline()
|
||||
pipe.Set(ctx, "key1", "value1", 0)
|
||||
pipe.Get(ctx, "key2")
|
||||
_, err := pipe.Exec(ctx)
|
||||
```
|
||||
|
||||
## Run the test
|
||||
|
||||
Recommended to use Docker, just need to run:
|
||||
```shell
|
||||
REDIS_PORT=9999 go test <your options>
|
||||
make test
|
||||
```
|
||||
|
||||
## See also
|
||||
@@ -285,6 +581,14 @@ REDIS_PORT=9999 go test <your options>
|
||||
|
||||
## Contributors
|
||||
|
||||
> The go-redis project was originally initiated by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
|
||||
> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can
|
||||
> use it to monitor applications and set up automatic alerts to receive notifications via email,
|
||||
> Slack, Telegram, and others.
|
||||
>
|
||||
> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which
|
||||
> demonstrates how you can use Uptrace to monitor go-redis.
|
||||
|
||||
Thanks to all the people who already contributed!
|
||||
|
||||
<a href="https://github.com/redis/go-redis/graphs/contributors">
|
||||
|
||||
+705
@@ -0,0 +1,705 @@
|
||||
# Release Notes
|
||||
|
||||
# 9.17.2 (2025-12-01)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **Connection Pool**: Fixed critical race condition in turn management that could cause connection leaks when dial goroutines complete after request timeout ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun)
|
||||
- **Context Timeout**: Improved context timeout calculation to use minimum of remaining time and DialTimeout, preventing goroutines from waiting longer than necessary ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun)
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- chore(deps): bump rojopolis/spellcheck-github-actions from 0.54.0 to 0.55.0 ([#3627](https://github.com/redis/go-redis/pull/3627))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@cyningsun](https://github.com/cyningsun) and [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.1...v9.17.2
|
||||
|
||||
# 9.17.1 (2025-11-25)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- add wait to keyless commands list ([#3615](https://github.com/redis/go-redis/pull/3615)) by [@marcoferrer](https://github.com/marcoferrer)
|
||||
- fix(time): remove cached time optimization ([#3611](https://github.com/redis/go-redis/pull/3611)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- chore(deps): bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 ([#3609](https://github.com/redis/go-redis/pull/3609))
|
||||
- chore(deps): bump actions/checkout from 5 to 6 ([#3610](https://github.com/redis/go-redis/pull/3610))
|
||||
- chore(script): fix help call in tag.sh ([#3606](https://github.com/redis/go-redis/pull/3606)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@marcoferrer](https://github.com/marcoferrer) and [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.17.1
|
||||
|
||||
# 9.17.0 (2025-11-19)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
### Redis 8.4 Support
|
||||
Added support for Redis 8.4, including new commands and features ([#3572](https://github.com/redis/go-redis/pull/3572))
|
||||
|
||||
### Typed Errors
|
||||
Introduced typed errors for better error handling using `errors.As` instead of string checks. Errors can now be wrapped and set to commands in hooks without breaking library functionality ([#3602](https://github.com/redis/go-redis/pull/3602))
|
||||
|
||||
### New Commands
|
||||
- **CAS/CAD Commands**: Added support for Compare-And-Set/Compare-And-Delete operations with conditional matching (`IFEQ`, `IFNE`, `IFDEQ`, `IFDNE`) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595))
|
||||
- **MSETEX**: Atomically set multiple key-value pairs with expiration options and conditional modes ([#3580](https://github.com/redis/go-redis/pull/3580))
|
||||
- **XReadGroup CLAIM**: Consume both incoming and idle pending entries from streams in a single call ([#3578](https://github.com/redis/go-redis/pull/3578))
|
||||
- **ACL Commands**: Added `ACLGenPass`, `ACLUsers`, and `ACLWhoAmI` ([#3576](https://github.com/redis/go-redis/pull/3576))
|
||||
- **SLOWLOG Commands**: Added `SLOWLOG LEN` and `SLOWLOG RESET` ([#3585](https://github.com/redis/go-redis/pull/3585))
|
||||
- **LATENCY Commands**: Added `LATENCY LATEST` and `LATENCY RESET` ([#3584](https://github.com/redis/go-redis/pull/3584))
|
||||
|
||||
### Search & Vector Improvements
|
||||
- **Hybrid Search**: Added **EXPERIMENTAL** support for the new `FT.HYBRID` command ([#3573](https://github.com/redis/go-redis/pull/3573))
|
||||
- **Vector Range**: Added `VRANGE` command for vector sets ([#3543](https://github.com/redis/go-redis/pull/3543))
|
||||
- **FT.INFO Enhancements**: Added vector-specific attributes in FT.INFO response ([#3596](https://github.com/redis/go-redis/pull/3596))
|
||||
|
||||
### Connection Pool Improvements
|
||||
- **Improved Connection Success Rate**: Implemented FIFO queue-based fairness and context pattern for connection creation to prevent premature cancellation under high concurrency ([#3518](https://github.com/redis/go-redis/pull/3518))
|
||||
- **Connection State Machine**: Resolved race conditions and improved pool performance with proper state tracking ([#3559](https://github.com/redis/go-redis/pull/3559))
|
||||
- **Pool Performance**: Significant performance improvements with faster semaphores, lockless hook manager, and reduced allocations (47-67% faster Get/Put operations) ([#3565](https://github.com/redis/go-redis/pull/3565))
|
||||
|
||||
### Metrics & Observability
|
||||
- **Canceled Metric Attribute**: Added 'canceled' metrics attribute to distinguish context cancellation errors from other errors ([#3566](https://github.com/redis/go-redis/pull/3566))
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
- Typed errors with wrapping support ([#3602](https://github.com/redis/go-redis/pull/3602)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- CAS/CAD commands (marked as experimental) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595)) by [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis)
|
||||
- MSETEX command support ([#3580](https://github.com/redis/go-redis/pull/3580)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- XReadGroup CLAIM argument ([#3578](https://github.com/redis/go-redis/pull/3578)) by [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
- ACL commands: GenPass, Users, WhoAmI ([#3576](https://github.com/redis/go-redis/pull/3576)) by [@destinyoooo](https://github.com/destinyoooo)
|
||||
- SLOWLOG commands: LEN, RESET ([#3585](https://github.com/redis/go-redis/pull/3585)) by [@destinyoooo](https://github.com/destinyoooo)
|
||||
- LATENCY commands: LATEST, RESET ([#3584](https://github.com/redis/go-redis/pull/3584)) by [@destinyoooo](https://github.com/destinyoooo)
|
||||
- Hybrid search command (FT.HYBRID) ([#3573](https://github.com/redis/go-redis/pull/3573)) by [@htemelski-redis](https://github.com/htemelski-redis)
|
||||
- Vector range command (VRANGE) ([#3543](https://github.com/redis/go-redis/pull/3543)) by [@cxljs](https://github.com/cxljs)
|
||||
- Vector-specific attributes in FT.INFO ([#3596](https://github.com/redis/go-redis/pull/3596)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- Improved connection pool success rate with FIFO queue ([#3518](https://github.com/redis/go-redis/pull/3518)) by [@cyningsun](https://github.com/cyningsun)
|
||||
- Canceled metrics attribute for context errors ([#3566](https://github.com/redis/go-redis/pull/3566)) by [@pvragov](https://github.com/pvragov)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- Fixed Failover Client MaintNotificationsConfig ([#3600](https://github.com/redis/go-redis/pull/3600)) by [@ajax16384](https://github.com/ajax16384)
|
||||
- Fixed ACLGenPass function to use the bit parameter ([#3597](https://github.com/redis/go-redis/pull/3597)) by [@destinyoooo](https://github.com/destinyoooo)
|
||||
- Return error instead of panic from commands ([#3568](https://github.com/redis/go-redis/pull/3568)) by [@dragneelfps](https://github.com/dragneelfps)
|
||||
- Safety harness in `joinErrors` to prevent panic ([#3577](https://github.com/redis/go-redis/pull/3577)) by [@manisharma](https://github.com/manisharma)
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
- Connection state machine with race condition fixes ([#3559](https://github.com/redis/go-redis/pull/3559)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- Pool performance improvements: 47-67% faster Get/Put, 33% less memory, 50% fewer allocations ([#3565](https://github.com/redis/go-redis/pull/3565)) by [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
## 🧪 Testing & Infrastructure
|
||||
|
||||
- Updated to Redis 8.4.0 image ([#3603](https://github.com/redis/go-redis/pull/3603)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- Added Redis 8.4-RC1-pre to CI ([#3572](https://github.com/redis/go-redis/pull/3572)) by [@ndyakov](https://github.com/ndyakov)
|
||||
- Refactored tests for idiomatic Go ([#3561](https://github.com/redis/go-redis/pull/3561), [#3562](https://github.com/redis/go-redis/pull/3562), [#3563](https://github.com/redis/go-redis/pull/3563)) by [@12ya](https://github.com/12ya)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@12ya](https://github.com/12ya), [@ajax16384](https://github.com/ajax16384), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@destinyoooo](https://github.com/destinyoooo), [@dragneelfps](https://github.com/dragneelfps), [@htemelski-redis](https://github.com/htemelski-redis), [@manisharma](https://github.com/manisharma), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@pvragov](https://github.com/pvragov)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.16.0...v9.17.0
|
||||
|
||||
# 9.16.0 (2025-10-23)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
### Maintenance Notifications Support
|
||||
|
||||
This release introduces comprehensive support for Redis maintenance notifications, enabling applications to handle server maintenance events gracefully. The new `maintnotifications` package provides:
|
||||
|
||||
- **RESP3 Push Notifications**: Full support for Redis RESP3 protocol push notifications
|
||||
- **Connection Handoff**: Automatic connection migration during server maintenance with configurable retry policies and circuit breakers
|
||||
- **Graceful Degradation**: Configurable timeout relaxation during maintenance windows to prevent false failures
|
||||
- **Event-Driven Architecture**: Background workers with on-demand scaling for efficient handoff processing
|
||||
- **Production-Ready**: Comprehensive E2E testing framework and monitoring capabilities
|
||||
|
||||
For detailed usage examples and configuration options, see the [maintenance notifications documentation](maintnotifications/README.md).
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
- **Trace Filtering**: Add support for filtering traces for specific commands, including pipeline operations and dial operations ([#3519](https://github.com/redis/go-redis/pull/3519), [#3550](https://github.com/redis/go-redis/pull/3550))
|
||||
- New `TraceCmdFilter` option to selectively trace commands
|
||||
- Reduces overhead by excluding high-frequency or low-value commands from traces
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **Pipeline Error Handling**: Fix issue where pipeline repeatedly sets the same error ([#3525](https://github.com/redis/go-redis/pull/3525))
|
||||
- **Connection Pool**: Ensure re-authentication does not interfere with connection handoff operations ([#3547](https://github.com/redis/go-redis/pull/3547))
|
||||
|
||||
## 🔧 Improvements
|
||||
|
||||
- **Hash Commands**: Update hash command implementations ([#3523](https://github.com/redis/go-redis/pull/3523))
|
||||
- **OpenTelemetry**: Use `metric.WithAttributeSet` to avoid unnecessary attribute copying in redisotel ([#3552](https://github.com/redis/go-redis/pull/3552))
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **Cluster Client**: Add explanation for why `MaxRetries` is disabled for `ClusterClient` ([#3551](https://github.com/redis/go-redis/pull/3551))
|
||||
|
||||
## 🧪 Testing & Infrastructure
|
||||
|
||||
- **E2E Testing**: Upgrade E2E testing framework with improved reliability and coverage ([#3541](https://github.com/redis/go-redis/pull/3541))
|
||||
- **Release Process**: Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530))
|
||||
|
||||
## 📦 Dependencies
|
||||
|
||||
- Bump `rojopolis/spellcheck-github-actions` from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520))
|
||||
- Bump `github/codeql-action` from 3 to 4 ([#3544](https://github.com/redis/go-redis/pull/3544))
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@Sovietaced](https://github.com/Sovietaced), [@Udhayarajan](https://github.com/Udhayarajan), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@Pika-Gopher](https://github.com/Pika-Gopher), [@cxljs](https://github.com/cxljs), [@huiyifyj](https://github.com/huiyifyj), [@omid-h70](https://github.com/omid-h70)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/redis/go-redis/compare/v9.14.0...v9.16.0
|
||||
|
||||
|
||||
# 9.15.0 was accidentally released. Please use version 9.16.0 instead.
|
||||
|
||||
# 9.15.0-beta.3 (2025-09-26)
|
||||
|
||||
## Highlights
|
||||
This beta release includes a pre-production version of processing push notifications and hitless upgrades.
|
||||
|
||||
# Changes
|
||||
|
||||
- chore: Update hash_commands.go ([#3523](https://github.com/redis/go-redis/pull/3523))
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
- feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- fix: pipeline repeatedly sets the error ([#3525](https://github.com/redis/go-redis/pull/3525))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- chore(deps): bump rojopolis/spellcheck-github-actions from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520))
|
||||
- feat(e2e-testing): maintnotifications e2e and refactor ([#3526](https://github.com/redis/go-redis/pull/3526))
|
||||
- feat(tag.sh): Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@cxljs](https://github.com/cxljs), [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), and [@omid-h70](https://github.com/omid-h70)
|
||||
|
||||
|
||||
# 9.15.0-beta.1 (2025-09-10)
|
||||
|
||||
## Highlights
|
||||
This beta release includes a pre-production version of processing push notifications and hitless upgrades.
|
||||
|
||||
### Hitless Upgrades
|
||||
Hitless upgrades is a major new feature that allows for zero-downtime upgrades in Redis clusters.
|
||||
You can find more information in the [Hitless Upgrades documentation](https://github.com/redis/go-redis/tree/master/hitless).
|
||||
|
||||
# Changes
|
||||
|
||||
## 🚀 New Features
|
||||
- [CAE-1088] & [CAE-1072] feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@ofekshenawa](https://github.com/ofekshenawa)
|
||||
|
||||
|
||||
# 9.14.0 (2025-09-10)
|
||||
|
||||
## Highlights
|
||||
- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510))
|
||||
|
||||
# Changes
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- fix: SetErr on Cmd if the command cannot be queued correctly in multi/exec ([#3509](https://github.com/redis/go-redis/pull/3509))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- Updates release drafter config to exclude dependabot ([#3511](https://github.com/redis/go-redis/pull/3511))
|
||||
- chore(deps): bump actions/setup-go from 5 to 6 ([#3504](https://github.com/redis/go-redis/pull/3504))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@elena-kolevska](https://github.com/elena-kolevksa), [@htemelski-redis](https://github.com/htemelski-redis) and [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
|
||||
# 9.13.0 (2025-09-03)
|
||||
|
||||
## Highlights
|
||||
- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496))
|
||||
- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470))
|
||||
- Fixes on Read and Write buffer sizes and UniversalOptions
|
||||
|
||||
## Changes
|
||||
- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496))
|
||||
- fix(test): fix a timing issue in pubsub test ([#3498](https://github.com/redis/go-redis/pull/3498))
|
||||
- Allow users to enable read-write splitting in failover mode. ([#3482](https://github.com/redis/go-redis/pull/3482))
|
||||
- Set the read/write buffer size of the sentinel client to 4KiB ([#3476](https://github.com/redis/go-redis/pull/3476))
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499))
|
||||
- Support subscriptions against cluster slave nodes ([#3480](https://github.com/redis/go-redis/pull/3480))
|
||||
- Add wait metrics to otel ([#3493](https://github.com/redis/go-redis/pull/3493))
|
||||
- Clean failing timeout implementation ([#3472](https://github.com/redis/go-redis/pull/3472))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- Do not assume that all non-IP hosts are loopbacks ([#3085](https://github.com/redis/go-redis/pull/3085))
|
||||
- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499))
|
||||
- fix(make test): Add default env in makefile ([#3491](https://github.com/redis/go-redis/pull/3491))
|
||||
- Update the introduction to running tests in README.md ([#3495](https://github.com/redis/go-redis/pull/3495))
|
||||
- test: Add comprehensive edge case tests for IncrByFloat command ([#3477](https://github.com/redis/go-redis/pull/3477))
|
||||
- Set the default read/write buffer size of Redis connection to 32KiB ([#3483](https://github.com/redis/go-redis/pull/3483))
|
||||
- Bumps test image to 8.2.1-pre ([#3478](https://github.com/redis/go-redis/pull/3478))
|
||||
- fix UniversalOptions miss ReadBufferSize and WriteBufferSize options ([#3485](https://github.com/redis/go-redis/pull/3485))
|
||||
- chore(deps): bump actions/checkout from 4 to 5 ([#3484](https://github.com/redis/go-redis/pull/3484))
|
||||
- Removes dry run for stale issues policy ([#3471](https://github.com/redis/go-redis/pull/3471))
|
||||
- Update otel metrics URL ([#3474](https://github.com/redis/go-redis/pull/3474))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@LINKIWI](https://github.com/LINKIWI), [@cxljs](https://github.com/cxljs), [@cybersmeashish](https://github.com/cybersmeashish), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@suever](https://github.com/suever)
|
||||
|
||||
|
||||
# 9.12.1 (2025-08-11)
|
||||
## 🚀 Highlights
|
||||
In the last version (9.12.0) the client introduced bigger write and read buffer sized. The default value we set was 512KiB.
|
||||
However, users reported that this is too big for most use cases and can lead to high memory usage.
|
||||
In this version the default value is changed to 256KiB. The `README.md` was updated to reflect the
|
||||
correct default value and include a note that the default value can be changed.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468))
|
||||
- chore: update & fix otel example ([#3466](https://github.com/redis/go-redis/pull/3466))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@ndyakov](https://github.com/ndyakov) and [@vmihailenco](https://github.com/vmihailenco)
|
||||
|
||||
# 9.12.0 (2025-08-05)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
- This release includes support for [Redis 8.2](https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.2-release-notes/).
|
||||
- Introduces an experimental Query Builders for `FTSearch`, `FTAggregate` and other search commands.
|
||||
- Adds support for `EPSILON` option in `FT.VSIM`.
|
||||
- Includes bug fixes and improvements contributed by the community related to ring and [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel).
|
||||
|
||||
## Changes
|
||||
- Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458))
|
||||
- chore(ci): Add 8.2 rc2 pre build for CI ([#3459](https://github.com/redis/go-redis/pull/3459))
|
||||
- Added new stream commands ([#3450](https://github.com/redis/go-redis/pull/3450))
|
||||
- feat: Add "skip_verify" to Sentinel ([#3428](https://github.com/redis/go-redis/pull/3428))
|
||||
- fix: `errors.Join` requires Go 1.20 or later ([#3442](https://github.com/redis/go-redis/pull/3442))
|
||||
- DOC-4344 document quickstart examples ([#3426](https://github.com/redis/go-redis/pull/3426))
|
||||
- feat(bitop): add support for the new bitop operations ([#3409](https://github.com/redis/go-redis/pull/3409))
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
- feat: recover addIdleConn may occur panic ([#2445](https://github.com/redis/go-redis/pull/2445))
|
||||
- feat(ring): specify custom health check func via HeartbeatFn option ([#2940](https://github.com/redis/go-redis/pull/2940))
|
||||
- Add Query Builder for RediSearch commands ([#3436](https://github.com/redis/go-redis/pull/3436))
|
||||
- add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453))
|
||||
- Add VAMANA vector type to RediSearch ([#3449](https://github.com/redis/go-redis/pull/3449))
|
||||
- VSIM add `EPSILON` option ([#3454](https://github.com/redis/go-redis/pull/3454))
|
||||
- Add closing support to otel metrics instrumentation ([#3444](https://github.com/redis/go-redis/pull/3444))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- fix(redisotel): fix buggy append in reportPoolStats ([#3122](https://github.com/redis/go-redis/pull/3122))
|
||||
- fix(search): return results even if doc is empty ([#3457](https://github.com/redis/go-redis/pull/3457))
|
||||
- [ISSUE-3402]: Ring.Pipelined return dial timeout error ([#3403](https://github.com/redis/go-redis/pull/3403))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- Merges stale issues jobs into one job with two steps ([#3463](https://github.com/redis/go-redis/pull/3463))
|
||||
- improve code readability ([#3446](https://github.com/redis/go-redis/pull/3446))
|
||||
- chore(release): 9.12.0-beta.1 ([#3460](https://github.com/redis/go-redis/pull/3460))
|
||||
- DOC-5472 time series doc examples ([#3443](https://github.com/redis/go-redis/pull/3443))
|
||||
- Add VAMANA compression algorithm tests ([#3461](https://github.com/redis/go-redis/pull/3461))
|
||||
- bumped redis 8.2 version used in the CI/CD ([#3451](https://github.com/redis/go-redis/pull/3451))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@monkey92t](https://github.com/monkey92t), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn), [@smnvdev](https://github.com/smnvdev), [@strobil](https://github.com/strobil) and [@wzy9607](https://github.com/wzy9607)
|
||||
|
||||
## New Contributors
|
||||
* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in [#3409](https://github.com/redis/go-redis/pull/3409)
|
||||
* [@smnvdev](https://github.com/smnvdev) made their first contribution in [#3403](https://github.com/redis/go-redis/pull/3403)
|
||||
* [@rokn](https://github.com/rokn) made their first contribution in [#3444](https://github.com/redis/go-redis/pull/3444)
|
||||
|
||||
# 9.11.0 (2025-06-24)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
Fixes TxPipeline to work correctly in cluster scenarios, allowing execution of commands
|
||||
only in the same slot.
|
||||
|
||||
# Changes
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
- Set cluster slot for `scan` commands, rather than random ([#2623](https://github.com/redis/go-redis/pull/2623))
|
||||
- Add CredentialsProvider field to UniversalOptions ([#2927](https://github.com/redis/go-redis/pull/2927))
|
||||
- feat(redisotel): add WithCallerEnabled option ([#3415](https://github.com/redis/go-redis/pull/3415))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- fix(txpipeline): keyless commands should take the slot of the keyed ([#3411](https://github.com/redis/go-redis/pull/3411))
|
||||
- fix(loading): cache the loaded flag for slave nodes ([#3410](https://github.com/redis/go-redis/pull/3410))
|
||||
- fix(txpipeline): should return error on multi/exec on multiple slots ([#3408](https://github.com/redis/go-redis/pull/3408))
|
||||
- fix: check if the shard exists to avoid returning nil ([#3396](https://github.com/redis/go-redis/pull/3396))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- feat: optimize connection pool waitTurn ([#3412](https://github.com/redis/go-redis/pull/3412))
|
||||
- chore(ci): update CI redis builds ([#3407](https://github.com/redis/go-redis/pull/3407))
|
||||
- chore: remove a redundant method from `Ring`, `Client` and `ClusterClient` ([#3401](https://github.com/redis/go-redis/pull/3401))
|
||||
- test: refactor TestBasicCredentials using table-driven tests ([#3406](https://github.com/redis/go-redis/pull/3406))
|
||||
- perf: reduce unnecessary memory allocation operations ([#3399](https://github.com/redis/go-redis/pull/3399))
|
||||
- fix: insert entry during iterating over a map ([#3398](https://github.com/redis/go-redis/pull/3398))
|
||||
- DOC-5229 probabilistic data type examples ([#3413](https://github.com/redis/go-redis/pull/3413))
|
||||
- chore(deps): bump rojopolis/spellcheck-github-actions from 0.49.0 to 0.51.0 ([#3414](https://github.com/redis/go-redis/pull/3414))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@andy-stark-redis](https://github.com/andy-stark-redis), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@cxljs](https://github.com/cxljs), [@dcherubini](https://github.com/dcherubini), [@dependabot[bot]](https://github.com/apps/dependabot), [@iamamirsalehi](https://github.com/iamamirsalehi), [@ndyakov](https://github.com/ndyakov), [@pete-woods](https://github.com/pete-woods), [@twz915](https://github.com/twz915) and [dependabot[bot]](https://github.com/apps/dependabot)
|
||||
|
||||
# 9.10.0 (2025-06-06)
|
||||
|
||||
## 🚀 Highlights
|
||||
|
||||
`go-redis` now supports [vector sets](https://redis.io/docs/latest/develop/data-types/vector-sets/). This data type is marked
|
||||
as "in preview" in Redis and its support in `go-redis` is marked as experimental. You can find examples in the documentation and
|
||||
in the `doctests` folder.
|
||||
|
||||
# Changes
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
- feat: support vectorset ([#3375](https://github.com/redis/go-redis/pull/3375))
|
||||
|
||||
## 🧰 Maintenance
|
||||
|
||||
- Add the missing NewFloatSliceResult for testing ([#3393](https://github.com/redis/go-redis/pull/3393))
|
||||
- DOC-5078 vector set examples ([#3394](https://github.com/redis/go-redis/pull/3394))
|
||||
|
||||
## Contributors
|
||||
We'd like to thank all the contributors who worked on this release!
|
||||
|
||||
[@AndBobsYourUncle](https://github.com/AndBobsYourUncle), [@andy-stark-redis](https://github.com/andy-stark-redis), [@fukua95](https://github.com/fukua95) and [@ndyakov](https://github.com/ndyakov)
|
||||
|
||||
|
||||
|
||||
# 9.9.0 (2025-05-27)
|
||||
|
||||
## 🚀 Highlights
|
||||
- **Token-based Authentication**: Added `StreamingCredentialsProvider` for dynamic credential updates (experimental)
|
||||
- Can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) for Azure AD authentication
|
||||
- **Connection Statistics**: Added connection waiting statistics for better monitoring
|
||||
- **Failover Improvements**: Added `ParseFailoverURL` for easier failover configuration
|
||||
- **Ring Client Enhancements**: Added shard access methods for better Pub/Sub management
|
||||
|
||||
## ✨ New Features
|
||||
- Added `StreamingCredentialsProvider` for token-based authentication ([#3320](https://github.com/redis/go-redis/pull/3320))
|
||||
- Supports dynamic credential updates
|
||||
- Includes connection close hooks
|
||||
- Note: Currently marked as experimental
|
||||
- Added `ParseFailoverURL` for parsing failover URLs ([#3362](https://github.com/redis/go-redis/pull/3362))
|
||||
- Added connection waiting statistics ([#2804](https://github.com/redis/go-redis/pull/2804))
|
||||
- Added new utility functions:
|
||||
- `ParseFloat` and `MustParseFloat` in public utils package ([#3371](https://github.com/redis/go-redis/pull/3371))
|
||||
- Unit tests for `Atoi`, `ParseInt`, `ParseUint`, and `ParseFloat` ([#3377](https://github.com/redis/go-redis/pull/3377))
|
||||
- Added Ring client shard access methods:
|
||||
- `GetShardClients()` to retrieve all active shard clients
|
||||
- `GetShardClientForKey(key string)` to get the shard client for a specific key ([#3388](https://github.com/redis/go-redis/pull/3388))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
- Fixed routing reads to loading slave nodes ([#3370](https://github.com/redis/go-redis/pull/3370))
|
||||
- Added support for nil lag in XINFO GROUPS ([#3369](https://github.com/redis/go-redis/pull/3369))
|
||||
- Fixed pool acquisition timeout issues ([#3381](https://github.com/redis/go-redis/pull/3381))
|
||||
- Optimized unnecessary copy operations ([#3376](https://github.com/redis/go-redis/pull/3376))
|
||||
|
||||
## 📚 Documentation
|
||||
- Updated documentation for XINFO GROUPS with nil lag support ([#3369](https://github.com/redis/go-redis/pull/3369))
|
||||
- Added package-level comments for new features
|
||||
|
||||
## ⚡ Performance and Reliability
|
||||
- Optimized `ReplaceSpaces` function ([#3383](https://github.com/redis/go-redis/pull/3383))
|
||||
- Set default value for `Options.Protocol` in `init()` ([#3387](https://github.com/redis/go-redis/pull/3387))
|
||||
- Exported pool errors for public consumption ([#3380](https://github.com/redis/go-redis/pull/3380))
|
||||
|
||||
## 🔧 Dependencies and Infrastructure
|
||||
- Updated Redis CI to version 8.0.1 ([#3372](https://github.com/redis/go-redis/pull/3372))
|
||||
- Updated spellcheck GitHub Actions ([#3389](https://github.com/redis/go-redis/pull/3389))
|
||||
- Removed unused parameters ([#3382](https://github.com/redis/go-redis/pull/3382), [#3384](https://github.com/redis/go-redis/pull/3384))
|
||||
|
||||
## 🧪 Testing
|
||||
- Added unit tests for pool acquisition timeout ([#3381](https://github.com/redis/go-redis/pull/3381))
|
||||
- Added unit tests for utility functions ([#3377](https://github.com/redis/go-redis/pull/3377))
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We would like to thank all the contributors who made this release possible:
|
||||
|
||||
[@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@LINKIWI](https://github.com/LINKIWI), [@iamamirsalehi](https://github.com/iamamirsalehi), [@fukua95](https://github.com/fukua95), [@lzakharov](https://github.com/lzakharov), [@DengY11](https://github.com/DengY11)
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
For a complete list of changes, see the [full changelog](https://github.com/redis/go-redis/compare/v9.8.0...v9.9.0).
|
||||
|
||||
# 9.8.0 (2025-04-30)
|
||||
|
||||
## 🚀 Highlights
|
||||
- **Redis 8 Support**: Full compatibility with Redis 8.0, including testing and CI integration
|
||||
- **Enhanced Hash Operations**: Added support for new hash commands (`HGETDEL`, `HGETEX`, `HSETEX`) and `HSTRLEN` command
|
||||
- **Search Improvements**: Enabled Search DIALECT 2 by default and added `CountOnly` argument for `FT.Search`
|
||||
|
||||
## ✨ New Features
|
||||
- Added support for new hash commands: `HGETDEL`, `HGETEX`, `HSETEX` ([#3305](https://github.com/redis/go-redis/pull/3305))
|
||||
- Added `HSTRLEN` command for hash operations ([#2843](https://github.com/redis/go-redis/pull/2843))
|
||||
- Added `Do` method for raw query by single connection from `pool.Conn()` ([#3182](https://github.com/redis/go-redis/pull/3182))
|
||||
- Prevent false-positive marshaling by treating zero time.Time as empty in isEmptyValue ([#3273](https://github.com/redis/go-redis/pull/3273))
|
||||
- Added FailoverClusterClient support for Universal client ([#2794](https://github.com/redis/go-redis/pull/2794))
|
||||
- Added support for cluster mode with `IsClusterMode` config parameter ([#3255](https://github.com/redis/go-redis/pull/3255))
|
||||
- Added client name support in `HELLO` RESP handshake ([#3294](https://github.com/redis/go-redis/pull/3294))
|
||||
- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213))
|
||||
- Added read-only option for failover configurations ([#3281](https://github.com/redis/go-redis/pull/3281))
|
||||
- Added `CountOnly` argument for `FT.Search` to use `LIMIT 0 0` ([#3338](https://github.com/redis/go-redis/pull/3338))
|
||||
- Added `DB` option support in `NewFailoverClusterClient` ([#3342](https://github.com/redis/go-redis/pull/3342))
|
||||
- Added `nil` check for the options when creating a client ([#3363](https://github.com/redis/go-redis/pull/3363))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
- Fixed `PubSub` concurrency safety issues ([#3360](https://github.com/redis/go-redis/pull/3360))
|
||||
- Fixed panic caused when argument is `nil` ([#3353](https://github.com/redis/go-redis/pull/3353))
|
||||
- Improved error handling when fetching master node from sentinels ([#3349](https://github.com/redis/go-redis/pull/3349))
|
||||
- Fixed connection pool timeout issues and increased retries ([#3298](https://github.com/redis/go-redis/pull/3298))
|
||||
- Fixed context cancellation error leading to connection spikes on Primary instances ([#3190](https://github.com/redis/go-redis/pull/3190))
|
||||
- Fixed RedisCluster client to consider `MASTERDOWN` a retriable error ([#3164](https://github.com/redis/go-redis/pull/3164))
|
||||
- Fixed tracing to show complete commands instead of truncated versions ([#3290](https://github.com/redis/go-redis/pull/3290))
|
||||
- Fixed OpenTelemetry instrumentation to prevent multiple span reporting ([#3168](https://github.com/redis/go-redis/pull/3168))
|
||||
- Fixed `FT.Search` Limit argument and added `CountOnly` argument for limit 0 0 ([#3338](https://github.com/redis/go-redis/pull/3338))
|
||||
- Fixed missing command in interface ([#3344](https://github.com/redis/go-redis/pull/3344))
|
||||
- Fixed slot calculation for `COUNTKEYSINSLOT` command ([#3327](https://github.com/redis/go-redis/pull/3327))
|
||||
- Updated PubSub implementation with correct context ([#3329](https://github.com/redis/go-redis/pull/3329))
|
||||
|
||||
## 📚 Documentation
|
||||
- Added hash search examples ([#3357](https://github.com/redis/go-redis/pull/3357))
|
||||
- Fixed documentation comments ([#3351](https://github.com/redis/go-redis/pull/3351))
|
||||
- Added `CountOnly` search example ([#3345](https://github.com/redis/go-redis/pull/3345))
|
||||
- Added examples for list commands: `LLEN`, `LPOP`, `LPUSH`, `LRANGE`, `RPOP`, `RPUSH` ([#3234](https://github.com/redis/go-redis/pull/3234))
|
||||
- Added `SADD` and `SMEMBERS` command examples ([#3242](https://github.com/redis/go-redis/pull/3242))
|
||||
- Updated `README.md` to use Redis Discord guild ([#3331](https://github.com/redis/go-redis/pull/3331))
|
||||
- Updated `HExpire` command documentation ([#3355](https://github.com/redis/go-redis/pull/3355))
|
||||
- Featured OpenTelemetry instrumentation more prominently ([#3316](https://github.com/redis/go-redis/pull/3316))
|
||||
- Updated `README.md` with additional information ([#310ce55](https://github.com/redis/go-redis/commit/310ce55))
|
||||
|
||||
## ⚡ Performance and Reliability
|
||||
- Bound connection pool background dials to configured dial timeout ([#3089](https://github.com/redis/go-redis/pull/3089))
|
||||
- Ensured context isn't exhausted via concurrent query ([#3334](https://github.com/redis/go-redis/pull/3334))
|
||||
|
||||
## 🔧 Dependencies and Infrastructure
|
||||
- Updated testing image to Redis 8.0-RC2 ([#3361](https://github.com/redis/go-redis/pull/3361))
|
||||
- Enabled CI for Redis CE 8.0 ([#3274](https://github.com/redis/go-redis/pull/3274))
|
||||
- Updated various dependencies:
|
||||
- Bumped golangci/golangci-lint-action from 6.5.0 to 7.0.0 ([#3354](https://github.com/redis/go-redis/pull/3354))
|
||||
- Bumped rojopolis/spellcheck-github-actions ([#3336](https://github.com/redis/go-redis/pull/3336))
|
||||
- Bumped golang.org/x/net in example/otel ([#3308](https://github.com/redis/go-redis/pull/3308))
|
||||
- Migrated golangci-lint configuration to v2 format ([#3354](https://github.com/redis/go-redis/pull/3354))
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213))
|
||||
- Dropped RedisGears (Triggers and Functions) support ([#3321](https://github.com/redis/go-redis/pull/3321))
|
||||
- Dropped FT.PROFILE command that was never enabled ([#3323](https://github.com/redis/go-redis/pull/3323))
|
||||
|
||||
## 🔒 Security
|
||||
- Fixed network error handling on SETINFO (CVE-2025-29923) ([#3295](https://github.com/redis/go-redis/pull/3295))
|
||||
|
||||
## 🧪 Testing
|
||||
- Added integration tests for Redis 8 behavior changes in Redis Search ([#3337](https://github.com/redis/go-redis/pull/3337))
|
||||
- Added vector types INT8 and UINT8 tests ([#3299](https://github.com/redis/go-redis/pull/3299))
|
||||
- Added test codes for search_commands.go ([#3285](https://github.com/redis/go-redis/pull/3285))
|
||||
- Fixed example test sorting ([#3292](https://github.com/redis/go-redis/pull/3292))
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
We would like to thank all the contributors who made this release possible:
|
||||
|
||||
[@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi)
|
||||
|
||||
|
||||
# Old Changelog
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980))
|
||||
Users can use the OpenTelemetry sampler to control the sampling behavior.
|
||||
For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep
|
||||
a similar behavior (drop orphan spans) of `go-redis` as before.
|
||||
|
||||
## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602))
|
||||
* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe))
|
||||
* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af))
|
||||
|
||||
|
||||
|
||||
## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e))
|
||||
* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8))
|
||||
* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af))
|
||||
|
||||
|
||||
|
||||
## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02)
|
||||
|
||||
### New Features
|
||||
|
||||
- feat(scan): scan time.Time sets the default decoding (#2413)
|
||||
- Add support for CLUSTER LINKS command (#2504)
|
||||
- Add support for acl dryrun command (#2502)
|
||||
- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500)
|
||||
- Add support for LCS Command (#2480)
|
||||
- Add support for BZMPOP (#2456)
|
||||
- Adding support for ZMPOP command (#2408)
|
||||
- Add support for LMPOP (#2440)
|
||||
- feat: remove pool unused fields (#2438)
|
||||
- Expiretime and PExpireTime (#2426)
|
||||
- Implement `FUNCTION` group of commands (#2475)
|
||||
- feat(zadd): add ZAddLT and ZAddGT (#2429)
|
||||
- Add: Support for COMMAND LIST command (#2491)
|
||||
- Add support for BLMPOP (#2442)
|
||||
- feat: check pipeline.Do to prevent confusion with Exec (#2517)
|
||||
- Function stats, function kill, fcall and fcall_ro (#2486)
|
||||
- feat: Add support for CLUSTER SHARDS command (#2507)
|
||||
- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498)
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix: eval api cmd.SetFirstKeyPos (#2501)
|
||||
- fix: limit the number of connections created (#2441)
|
||||
- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479)
|
||||
- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458)
|
||||
- fix: group lag can be null (#2448)
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Updating to the latest version of redis (#2508)
|
||||
- Allowing for running tests on a port other than the fixed 6380 (#2466)
|
||||
- redis 7.0.8 in tests (#2450)
|
||||
- docs: Update redisotel example for v9 (#2425)
|
||||
- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476)
|
||||
- chore: add Chinese translation (#2436)
|
||||
- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421)
|
||||
- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420)
|
||||
- chore(deps): bump actions/setup-go from 3 to 4 (#2495)
|
||||
- docs: add instructions for the HSet api (#2503)
|
||||
- docs: add reading lag field comment (#2451)
|
||||
- test: update go mod before testing(go mod tidy) (#2423)
|
||||
- docs: fix comment typo (#2505)
|
||||
- test: remove testify (#2463)
|
||||
- refactor: change ListElementCmd to KeyValuesCmd. (#2443)
|
||||
- fix(appendArg): appendArg case special type (#2489)
|
||||
|
||||
## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01)
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65))
|
||||
|
||||
## v9 2023-01-30
|
||||
|
||||
### Breaking
|
||||
|
||||
- Changed Pipelines to not be thread-safe any more.
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was
|
||||
contributed by @monkey92t who has done the majority of work in this release.
|
||||
- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts
|
||||
and deadlines. See
|
||||
[Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details.
|
||||
- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example,
|
||||
`redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`.
|
||||
- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See
|
||||
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html)
|
||||
- Added `redis.HasErrorPrefix` to help working with errors.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is
|
||||
completely gone in v9.
|
||||
- Reworked hook interface and added `DialHook`.
|
||||
- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See
|
||||
[example](example/otel) and
|
||||
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html).
|
||||
- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making
|
||||
an allocation.
|
||||
- Renamed the option `MaxConnAge` to `ConnMaxLifetime`.
|
||||
- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`.
|
||||
- Removed connection reaper in favor of `MaxIdleConns`.
|
||||
- Removed `WithContext` since `context.Context` can be passed directly as an arg.
|
||||
- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and
|
||||
it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to
|
||||
reset commands for some reason.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved and fixed pipeline retries.
|
||||
- As usually, added support for more commands and fixed some bugs.
|
||||
+81
@@ -4,8 +4,24 @@ import "context"
|
||||
|
||||
type ACLCmdable interface {
|
||||
ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd
|
||||
|
||||
ACLLog(ctx context.Context, count int64) *ACLLogCmd
|
||||
ACLLogReset(ctx context.Context) *StatusCmd
|
||||
|
||||
ACLGenPass(ctx context.Context, bit int) *StringCmd
|
||||
|
||||
ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd
|
||||
ACLDelUser(ctx context.Context, username string) *IntCmd
|
||||
ACLUsers(ctx context.Context) *StringSliceCmd
|
||||
ACLWhoAmI(ctx context.Context) *StringCmd
|
||||
ACLList(ctx context.Context) *StringSliceCmd
|
||||
|
||||
ACLCat(ctx context.Context) *StringSliceCmd
|
||||
ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd
|
||||
}
|
||||
|
||||
type ACLCatArgs struct {
|
||||
Category string
|
||||
}
|
||||
|
||||
func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd {
|
||||
@@ -33,3 +49,68 @@ func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd {
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "acl", "deluser", username)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd {
|
||||
args := make([]interface{}, 3+len(rules))
|
||||
args[0] = "acl"
|
||||
args[1] = "setuser"
|
||||
args[2] = username
|
||||
for i, rule := range rules {
|
||||
args[i+3] = rule
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLGenPass(ctx context.Context, bit int) *StringCmd {
|
||||
args := make([]interface{}, 0, 3)
|
||||
args = append(args, "acl", "genpass")
|
||||
if bit > 0 {
|
||||
args = append(args, bit)
|
||||
}
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLUsers(ctx context.Context) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "acl", "users")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLWhoAmI(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "acl", "whoami")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "acl", "list")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "acl", "cat")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd {
|
||||
// if there is a category passed, build new cmd, if there isn't - use the ACLCat method
|
||||
if options != nil && options.Category != "" {
|
||||
cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
return c.ACLCat(ctx)
|
||||
}
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/interfaces"
|
||||
"github.com/redis/go-redis/v9/push"
|
||||
)
|
||||
|
||||
// ErrInvalidCommand is returned when an invalid command is passed to ExecuteCommand.
|
||||
var ErrInvalidCommand = errors.New("invalid command type")
|
||||
|
||||
// ErrInvalidPool is returned when the pool type is not supported.
|
||||
var ErrInvalidPool = errors.New("invalid pool type")
|
||||
|
||||
// newClientAdapter creates a new client adapter for regular Redis clients.
|
||||
func newClientAdapter(client *baseClient) interfaces.ClientInterface {
|
||||
return &clientAdapter{client: client}
|
||||
}
|
||||
|
||||
// clientAdapter adapts a Redis client to implement interfaces.ClientInterface.
|
||||
type clientAdapter struct {
|
||||
client *baseClient
|
||||
}
|
||||
|
||||
// GetOptions returns the client options.
|
||||
func (ca *clientAdapter) GetOptions() interfaces.OptionsInterface {
|
||||
return &optionsAdapter{options: ca.client.opt}
|
||||
}
|
||||
|
||||
// GetPushProcessor returns the client's push notification processor.
|
||||
func (ca *clientAdapter) GetPushProcessor() interfaces.NotificationProcessor {
|
||||
return &pushProcessorAdapter{processor: ca.client.pushProcessor}
|
||||
}
|
||||
|
||||
// optionsAdapter adapts Redis options to implement interfaces.OptionsInterface.
|
||||
type optionsAdapter struct {
|
||||
options *Options
|
||||
}
|
||||
|
||||
// GetReadTimeout returns the read timeout.
|
||||
func (oa *optionsAdapter) GetReadTimeout() time.Duration {
|
||||
return oa.options.ReadTimeout
|
||||
}
|
||||
|
||||
// GetWriteTimeout returns the write timeout.
|
||||
func (oa *optionsAdapter) GetWriteTimeout() time.Duration {
|
||||
return oa.options.WriteTimeout
|
||||
}
|
||||
|
||||
// GetNetwork returns the network type.
|
||||
func (oa *optionsAdapter) GetNetwork() string {
|
||||
return oa.options.Network
|
||||
}
|
||||
|
||||
// GetAddr returns the connection address.
|
||||
func (oa *optionsAdapter) GetAddr() string {
|
||||
return oa.options.Addr
|
||||
}
|
||||
|
||||
// IsTLSEnabled returns true if TLS is enabled.
|
||||
func (oa *optionsAdapter) IsTLSEnabled() bool {
|
||||
return oa.options.TLSConfig != nil
|
||||
}
|
||||
|
||||
// GetProtocol returns the protocol version.
|
||||
func (oa *optionsAdapter) GetProtocol() int {
|
||||
return oa.options.Protocol
|
||||
}
|
||||
|
||||
// GetPoolSize returns the connection pool size.
|
||||
func (oa *optionsAdapter) GetPoolSize() int {
|
||||
return oa.options.PoolSize
|
||||
}
|
||||
|
||||
// NewDialer returns a new dialer function for the connection.
|
||||
func (oa *optionsAdapter) NewDialer() func(context.Context) (net.Conn, error) {
|
||||
baseDialer := oa.options.NewDialer()
|
||||
return func(ctx context.Context) (net.Conn, error) {
|
||||
// Extract network and address from the options
|
||||
network := oa.options.Network
|
||||
addr := oa.options.Addr
|
||||
return baseDialer(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// pushProcessorAdapter adapts a push.NotificationProcessor to implement interfaces.NotificationProcessor.
|
||||
type pushProcessorAdapter struct {
|
||||
processor push.NotificationProcessor
|
||||
}
|
||||
|
||||
// RegisterHandler registers a handler for a specific push notification name.
|
||||
func (ppa *pushProcessorAdapter) RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error {
|
||||
if pushHandler, ok := handler.(push.NotificationHandler); ok {
|
||||
return ppa.processor.RegisterHandler(pushNotificationName, pushHandler, protected)
|
||||
}
|
||||
return errors.New("handler must implement push.NotificationHandler")
|
||||
}
|
||||
|
||||
// UnregisterHandler removes a handler for a specific push notification name.
|
||||
func (ppa *pushProcessorAdapter) UnregisterHandler(pushNotificationName string) error {
|
||||
return ppa.processor.UnregisterHandler(pushNotificationName)
|
||||
}
|
||||
|
||||
// GetHandler returns the handler for a specific push notification name.
|
||||
func (ppa *pushProcessorAdapter) GetHandler(pushNotificationName string) interface{} {
|
||||
return ppa.processor.GetHandler(pushNotificationName)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Package auth package provides authentication-related interfaces and types.
|
||||
// It also includes a basic implementation of credentials using username and password.
|
||||
package auth
|
||||
|
||||
// StreamingCredentialsProvider is an interface that defines the methods for a streaming credentials provider.
|
||||
// It is used to provide credentials for authentication.
|
||||
// The CredentialsListener is used to receive updates when the credentials change.
|
||||
type StreamingCredentialsProvider interface {
|
||||
// Subscribe subscribes to the credentials provider for updates.
|
||||
// It returns the current credentials, a cancel function to unsubscribe from the provider,
|
||||
// and an error if any.
|
||||
// TODO(ndyakov): Should we add context to the Subscribe method?
|
||||
Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error)
|
||||
}
|
||||
|
||||
// UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider.
|
||||
// It is used to unsubscribe from the provider when the credentials are no longer needed.
|
||||
type UnsubscribeFunc func() error
|
||||
|
||||
// CredentialsListener is an interface that defines the methods for a credentials listener.
|
||||
// It is used to receive updates when the credentials change.
|
||||
// The OnNext method is called when the credentials change.
|
||||
// The OnError method is called when an error occurs while requesting the credentials.
|
||||
type CredentialsListener interface {
|
||||
OnNext(credentials Credentials)
|
||||
OnError(err error)
|
||||
}
|
||||
|
||||
// Credentials is an interface that defines the methods for credentials.
|
||||
// It is used to provide the credentials for authentication.
|
||||
type Credentials interface {
|
||||
// BasicAuth returns the username and password for basic authentication.
|
||||
BasicAuth() (username string, password string)
|
||||
// RawCredentials returns the raw credentials as a string.
|
||||
// This can be used to extract the username and password from the raw credentials or
|
||||
// additional information if present in the token.
|
||||
RawCredentials() string
|
||||
}
|
||||
|
||||
type basicAuth struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
// RawCredentials returns the raw credentials as a string.
|
||||
func (b *basicAuth) RawCredentials() string {
|
||||
return b.username + ":" + b.password
|
||||
}
|
||||
|
||||
// BasicAuth returns the username and password for basic authentication.
|
||||
func (b *basicAuth) BasicAuth() (username string, password string) {
|
||||
return b.username, b.password
|
||||
}
|
||||
|
||||
// NewBasicCredentials creates a new Credentials object from the given username and password.
|
||||
func NewBasicCredentials(username, password string) Credentials {
|
||||
return &basicAuth{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package auth
|
||||
|
||||
// ReAuthCredentialsListener is a struct that implements the CredentialsListener interface.
|
||||
// It is used to re-authenticate the credentials when they are updated.
|
||||
// It contains:
|
||||
// - reAuth: a function that takes the new credentials and returns an error if any.
|
||||
// - onErr: a function that takes an error and handles it.
|
||||
type ReAuthCredentialsListener struct {
|
||||
reAuth func(credentials Credentials) error
|
||||
onErr func(err error)
|
||||
}
|
||||
|
||||
// OnNext is called when the credentials are updated.
|
||||
// It calls the reAuth function with the new credentials.
|
||||
// If the reAuth function returns an error, it calls the onErr function with the error.
|
||||
func (c *ReAuthCredentialsListener) OnNext(credentials Credentials) {
|
||||
if c.reAuth == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := c.reAuth(credentials)
|
||||
if err != nil {
|
||||
c.OnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// OnError is called when an error occurs.
|
||||
// It can be called from both the credentials provider and the reAuth function.
|
||||
func (c *ReAuthCredentialsListener) OnError(err error) {
|
||||
if c.onErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.onErr(err)
|
||||
}
|
||||
|
||||
// NewReAuthCredentialsListener creates a new ReAuthCredentialsListener.
|
||||
// Implements the auth.CredentialsListener interface.
|
||||
func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, onErr func(err error)) *ReAuthCredentialsListener {
|
||||
return &ReAuthCredentialsListener{
|
||||
reAuth: reAuth,
|
||||
onErr: onErr,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure ReAuthCredentialsListener implements the CredentialsListener interface.
|
||||
var _ CredentialsListener = (*ReAuthCredentialsListener)(nil)
|
||||
+38
-2
@@ -12,6 +12,10 @@ type BitMapCmdable interface {
|
||||
BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpNot(ctx context.Context, destKey string, key string) *IntCmd
|
||||
BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd
|
||||
BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd
|
||||
@@ -78,22 +82,50 @@ func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BitOpAnd creates a new bitmap in which users are members of all given bitmaps
|
||||
func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "and", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitOpOr creates a new bitmap in which users are member of at least one given bitmap
|
||||
func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "or", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitOpXor creates a new bitmap in which users are the result of XORing all given bitmaps
|
||||
func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "xor", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitOpNot creates a new bitmap in which users are not members of a given bitmap
|
||||
func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd {
|
||||
return c.bitOp(ctx, "not", destKey, key)
|
||||
}
|
||||
|
||||
// BitOpDiff creates a new bitmap in which users are members of bitmap X but not of any of bitmaps Y1, Y2, …
|
||||
// Introduced with Redis 8.2
|
||||
func (c cmdable) BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "diff", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitOpDiff1 creates a new bitmap in which users are members of one or more of bitmaps Y1, Y2, … but not members of bitmap X
|
||||
// Introduced with Redis 8.2
|
||||
func (c cmdable) BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "diff1", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitOpAndOr creates a new bitmap in which users are members of bitmap X and also members of one or more of bitmaps Y1, Y2, …
|
||||
// Introduced with Redis 8.2
|
||||
func (c cmdable) BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "andor", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitOpOne creates a new bitmap in which users are members of exactly one of the given bitmaps
|
||||
// Introduced with Redis 8.2
|
||||
func (c cmdable) BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "one", destKey, keys...)
|
||||
}
|
||||
|
||||
// BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end
|
||||
// if you need the `byte | bit` parameter, please use `BitPosSpan`.
|
||||
func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd {
|
||||
@@ -109,7 +141,9 @@ func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64
|
||||
args[3] = pos[0]
|
||||
args[4] = pos[1]
|
||||
default:
|
||||
panic("too many arguments")
|
||||
cmd := NewIntCmd(ctx)
|
||||
cmd.SetErr(errors.New("too many arguments"))
|
||||
return cmd
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
@@ -150,7 +184,9 @@ func (c cmdable) BitFieldRO(ctx context.Context, key string, values ...interface
|
||||
args[0] = "BITFIELD_RO"
|
||||
args[1] = key
|
||||
if len(values)%2 != 0 {
|
||||
panic("BitFieldRO: invalid number of arguments, must be even")
|
||||
c := NewIntSliceCmd(ctx)
|
||||
c.SetErr(errors.New("BitFieldRO: invalid number of arguments, must be even"))
|
||||
return c
|
||||
}
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
args = append(args, "GET", values[i], values[i+1])
|
||||
|
||||
+7
@@ -4,6 +4,7 @@ import "context"
|
||||
|
||||
type ClusterCmdable interface {
|
||||
ClusterMyShardID(ctx context.Context) *StringCmd
|
||||
ClusterMyID(ctx context.Context) *StringCmd
|
||||
ClusterSlots(ctx context.Context) *ClusterSlotsCmd
|
||||
ClusterShards(ctx context.Context) *ClusterShardsCmd
|
||||
ClusterLinks(ctx context.Context) *ClusterLinksCmd
|
||||
@@ -35,6 +36,12 @@ func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "cluster", "myid")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
|
||||
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
|
||||
_ = c(ctx, cmd)
|
||||
|
||||
+392
-40
@@ -17,6 +17,56 @@ import (
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// keylessCommands contains Redis commands that have empty key specifications (9th slot empty)
|
||||
// Only includes core Redis commands, excludes FT.*, ts.*, timeseries.*, search.* and subcommands
|
||||
var keylessCommands = map[string]struct{}{
|
||||
"acl": {},
|
||||
"asking": {},
|
||||
"auth": {},
|
||||
"bgrewriteaof": {},
|
||||
"bgsave": {},
|
||||
"client": {},
|
||||
"cluster": {},
|
||||
"config": {},
|
||||
"debug": {},
|
||||
"discard": {},
|
||||
"echo": {},
|
||||
"exec": {},
|
||||
"failover": {},
|
||||
"function": {},
|
||||
"hello": {},
|
||||
"latency": {},
|
||||
"lolwut": {},
|
||||
"module": {},
|
||||
"monitor": {},
|
||||
"multi": {},
|
||||
"pfselftest": {},
|
||||
"ping": {},
|
||||
"psubscribe": {},
|
||||
"psync": {},
|
||||
"publish": {},
|
||||
"pubsub": {},
|
||||
"punsubscribe": {},
|
||||
"quit": {},
|
||||
"readonly": {},
|
||||
"readwrite": {},
|
||||
"replconf": {},
|
||||
"replicaof": {},
|
||||
"role": {},
|
||||
"save": {},
|
||||
"script": {},
|
||||
"select": {},
|
||||
"shutdown": {},
|
||||
"slaveof": {},
|
||||
"slowlog": {},
|
||||
"subscribe": {},
|
||||
"swapdb": {},
|
||||
"sync": {},
|
||||
"unsubscribe": {},
|
||||
"unwatch": {},
|
||||
"wait": {},
|
||||
}
|
||||
|
||||
type Cmder interface {
|
||||
// command name.
|
||||
// e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster".
|
||||
@@ -75,12 +125,22 @@ func writeCmd(wr *proto.Writer, cmd Cmder) error {
|
||||
return wr.WriteArgs(cmd.Args())
|
||||
}
|
||||
|
||||
// cmdFirstKeyPos returns the position of the first key in the command's arguments.
|
||||
// If the command does not have a key, it returns 0.
|
||||
// TODO: Use the data in CommandInfo to determine the first key position.
|
||||
func cmdFirstKeyPos(cmd Cmder) int {
|
||||
if pos := cmd.firstKeyPos(); pos != 0 {
|
||||
return int(pos)
|
||||
}
|
||||
|
||||
switch cmd.Name() {
|
||||
name := cmd.Name()
|
||||
|
||||
// first check if the command is keyless
|
||||
if _, ok := keylessCommands[name]; ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "eval", "evalsha", "eval_ro", "evalsha_ro":
|
||||
if cmd.stringArg(2) != "0" {
|
||||
return 3
|
||||
@@ -639,6 +699,68 @@ func (cmd *IntCmd) readReply(rd *proto.Reader) (err error) {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// DigestCmd is a command that returns a uint64 xxh3 hash digest.
|
||||
//
|
||||
// This command is specifically designed for the Redis DIGEST command,
|
||||
// which returns the xxh3 hash of a key's value as a hex string.
|
||||
// The hex string is automatically parsed to a uint64 value.
|
||||
//
|
||||
// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE,
|
||||
// and DelExArgs commands.
|
||||
//
|
||||
// For examples of client-side digest generation and usage patterns, see:
|
||||
// example/digest-optimistic-locking/
|
||||
//
|
||||
// Redis 8.4+. See https://redis.io/commands/digest/
|
||||
type DigestCmd struct {
|
||||
baseCmd
|
||||
|
||||
val uint64
|
||||
}
|
||||
|
||||
var _ Cmder = (*DigestCmd)(nil)
|
||||
|
||||
func NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd {
|
||||
return &DigestCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *DigestCmd) SetVal(val uint64) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *DigestCmd) Val() uint64 {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *DigestCmd) Result() (uint64, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *DigestCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) {
|
||||
// Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890")
|
||||
// We parse it as a uint64 xxh3 hash value
|
||||
var hexStr string
|
||||
hexStr, err = rd.ReadString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse hex string to uint64
|
||||
cmd.val, err = strconv.ParseUint(hexStr, 16, 64)
|
||||
return err
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type IntSliceCmd struct {
|
||||
baseCmd
|
||||
|
||||
@@ -1405,27 +1527,64 @@ func (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} {
|
||||
}
|
||||
|
||||
func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) {
|
||||
n, err := rd.ReadMapLen()
|
||||
readType, err := rd.PeekReplyType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = make(map[string][]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
k, err := rd.ReadString()
|
||||
|
||||
cmd.val = make(map[string][]interface{})
|
||||
|
||||
switch readType {
|
||||
case proto.RespMap:
|
||||
n, err := rd.ReadMapLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nn, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[k] = make([]interface{}, nn)
|
||||
for j := 0; j < nn; j++ {
|
||||
value, err := rd.ReadReply()
|
||||
for i := 0; i < n; i++ {
|
||||
k, err := rd.ReadString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[k][j] = value
|
||||
nn, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[k] = make([]interface{}, nn)
|
||||
for j := 0; j < nn; j++ {
|
||||
value, err := rd.ReadReply()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[k][j] = value
|
||||
}
|
||||
}
|
||||
case proto.RespArray:
|
||||
// RESP2 response
|
||||
n, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
// Each entry in this array is itself an array with key details
|
||||
itemLen, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := rd.ReadString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[key] = make([]interface{}, 0, itemLen-1)
|
||||
for j := 1; j < itemLen; j++ {
|
||||
// Read the inner array for timestamp-value pairs
|
||||
data, err := rd.ReadReply()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[key] = append(cmd.val[key], data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1489,6 +1648,12 @@ func (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error {
|
||||
type XMessage struct {
|
||||
ID string
|
||||
Values map[string]interface{}
|
||||
// MillisElapsedFromDelivery is the number of milliseconds since the entry was last delivered.
|
||||
// Only populated when using XREADGROUP with CLAIM argument for claimed entries.
|
||||
MillisElapsedFromDelivery int64
|
||||
// DeliveredCount is the number of times the entry was delivered.
|
||||
// Only populated when using XREADGROUP with CLAIM argument for claimed entries.
|
||||
DeliveredCount int64
|
||||
}
|
||||
|
||||
type XMessageSliceCmd struct {
|
||||
@@ -1545,10 +1710,16 @@ func readXMessageSlice(rd *proto.Reader) ([]XMessage, error) {
|
||||
}
|
||||
|
||||
func readXMessage(rd *proto.Reader) (XMessage, error) {
|
||||
if err := rd.ReadFixedArrayLen(2); err != nil {
|
||||
// Read array length can be 2 or 4 (with CLAIM metadata)
|
||||
n, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return XMessage{}, err
|
||||
}
|
||||
|
||||
if n != 2 && n != 4 {
|
||||
return XMessage{}, fmt.Errorf("redis: got %d elements in the XMessage array, expected 2 or 4", n)
|
||||
}
|
||||
|
||||
id, err := rd.ReadString()
|
||||
if err != nil {
|
||||
return XMessage{}, err
|
||||
@@ -1561,10 +1732,24 @@ func readXMessage(rd *proto.Reader) (XMessage, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return XMessage{
|
||||
msg := XMessage{
|
||||
ID: id,
|
||||
Values: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if n == 4 {
|
||||
msg.MillisElapsedFromDelivery, err = rd.ReadInt()
|
||||
if err != nil {
|
||||
return XMessage{}, err
|
||||
}
|
||||
|
||||
msg.DeliveredCount, err = rd.ReadInt()
|
||||
if err != nil {
|
||||
return XMessage{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func stringInterfaceMapParser(rd *proto.Reader) (map[string]interface{}, error) {
|
||||
@@ -2067,7 +2252,9 @@ type XInfoGroup struct {
|
||||
Pending int64
|
||||
LastDeliveredID string
|
||||
EntriesRead int64
|
||||
Lag int64
|
||||
// Lag represents the number of pending messages in the stream not yet
|
||||
// delivered to this consumer group. Returns -1 when the lag cannot be determined.
|
||||
Lag int64
|
||||
}
|
||||
|
||||
var _ Cmder = (*XInfoGroupsCmd)(nil)
|
||||
@@ -2150,8 +2337,11 @@ func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error {
|
||||
|
||||
// lag: the number of entries in the stream that are still waiting to be delivered
|
||||
// to the group's consumers, or a NULL(Nil) when that number can't be determined.
|
||||
// In that case, we return -1.
|
||||
if err != nil && err != Nil {
|
||||
return err
|
||||
} else if err == Nil {
|
||||
group.Lag = -1
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("redis: unexpected key %q in XINFO GROUPS reply", key)
|
||||
@@ -3542,15 +3732,14 @@ func (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error
|
||||
return err
|
||||
}
|
||||
|
||||
lowerCmds := make(map[string]*CommandInfo, len(cmds))
|
||||
|
||||
// Extensions have cmd names in upper case. Convert them to lower case.
|
||||
for k, v := range cmds {
|
||||
lower := internal.ToLower(k)
|
||||
if lower != k {
|
||||
cmds[lower] = v
|
||||
}
|
||||
lowerCmds[internal.ToLower(k)] = v
|
||||
}
|
||||
|
||||
c.cmds = cmds
|
||||
c.cmds = lowerCmds
|
||||
return nil
|
||||
})
|
||||
return c.cmds, err
|
||||
@@ -3668,6 +3857,83 @@ func (cmd *SlowLogCmd) readReply(rd *proto.Reader) error {
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
type Latency struct {
|
||||
Name string
|
||||
Time time.Time
|
||||
Latest time.Duration
|
||||
Max time.Duration
|
||||
}
|
||||
|
||||
type LatencyCmd struct {
|
||||
baseCmd
|
||||
val []Latency
|
||||
}
|
||||
|
||||
var _ Cmder = (*LatencyCmd)(nil)
|
||||
|
||||
func NewLatencyCmd(ctx context.Context, args ...interface{}) *LatencyCmd {
|
||||
return &LatencyCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *LatencyCmd) SetVal(val []Latency) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *LatencyCmd) Val() []Latency {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *LatencyCmd) Result() ([]Latency, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *LatencyCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *LatencyCmd) readReply(rd *proto.Reader) error {
|
||||
n, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = make([]Latency, n)
|
||||
for i := 0; i < len(cmd.val); i++ {
|
||||
nn, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if nn < 3 {
|
||||
return fmt.Errorf("redis: got %d elements in latency get, expected at least 3", nn)
|
||||
}
|
||||
if cmd.val[i].Name, err = rd.ReadString(); err != nil {
|
||||
return err
|
||||
}
|
||||
createdAt, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[i].Time = time.Unix(createdAt, 0)
|
||||
latest, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[i].Latest = time.Duration(latest) * time.Millisecond
|
||||
maximum, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[i].Max = time.Duration(maximum) * time.Millisecond
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
type MapStringInterfaceCmd struct {
|
||||
baseCmd
|
||||
|
||||
@@ -3795,7 +4061,8 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// MapStringInterfaceCmd represents a command that returns a map of strings to interface{}.
|
||||
|
||||
// MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}.
|
||||
type MapMapStringInterfaceCmd struct {
|
||||
baseCmd
|
||||
val map[string]interface{}
|
||||
@@ -3826,30 +4093,48 @@ func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3
|
||||
func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) {
|
||||
n, err := rd.ReadArrayLen()
|
||||
data, err := rd.ReadReply()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resultMap := map[string]interface{}{}
|
||||
|
||||
data := make(map[string]interface{}, n/2)
|
||||
for i := 0; i < n; i += 2 {
|
||||
_, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
cmd.err = err
|
||||
switch midResponse := data.(type) {
|
||||
case map[interface{}]interface{}: // resp3 will return map
|
||||
for k, v := range midResponse {
|
||||
stringKey, ok := k.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("redis: invalid map key %#v", k)
|
||||
}
|
||||
resultMap[stringKey] = v
|
||||
}
|
||||
key, err := rd.ReadString()
|
||||
if err != nil {
|
||||
cmd.err = err
|
||||
case []interface{}: // resp2 will return array of arrays
|
||||
n := len(midResponse)
|
||||
for i := 0; i < n; i++ {
|
||||
finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map
|
||||
if !ok {
|
||||
return fmt.Errorf("redis: unexpected response %#v", data)
|
||||
}
|
||||
m := len(finalArr)
|
||||
if m%2 != 0 { // since this should be map, keys should be even number
|
||||
return fmt.Errorf("redis: unexpected response %#v", data)
|
||||
}
|
||||
|
||||
for j := 0; j < m; j += 2 {
|
||||
stringKey, ok := finalArr[j].(string) // the first one
|
||||
if !ok {
|
||||
return fmt.Errorf("redis: invalid map key %#v", finalArr[i])
|
||||
}
|
||||
resultMap[stringKey] = finalArr[j+1] // second one is value
|
||||
}
|
||||
}
|
||||
value, err := rd.ReadString()
|
||||
if err != nil {
|
||||
cmd.err = err
|
||||
}
|
||||
data[key] = value
|
||||
default:
|
||||
return fmt.Errorf("redis: unexpected response %#v", data)
|
||||
}
|
||||
|
||||
cmd.val = data
|
||||
cmd.val = resultMap
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5078,6 +5363,10 @@ type ClientInfo struct {
|
||||
OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full)
|
||||
OutputMemory int // omem, output buffer memory usage
|
||||
TotalMemory int // tot-mem, total memory consumed by this client in its various buffers
|
||||
TotalNetIn int // tot-net-in, total network input
|
||||
TotalNetOut int // tot-net-out, total network output
|
||||
TotalCmds int // tot-cmds, total number of commands processed
|
||||
IoThread int // io-thread id
|
||||
Events string // file descriptor events (see below)
|
||||
LastCmd string // cmd, last command played
|
||||
User string // the authenticated username of the client
|
||||
@@ -5242,6 +5531,12 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) {
|
||||
info.OutputMemory, err = strconv.Atoi(val)
|
||||
case "tot-mem":
|
||||
info.TotalMemory, err = strconv.Atoi(val)
|
||||
case "tot-net-in":
|
||||
info.TotalNetIn, err = strconv.Atoi(val)
|
||||
case "tot-net-out":
|
||||
info.TotalNetOut, err = strconv.Atoi(val)
|
||||
case "tot-cmds":
|
||||
info.TotalCmds, err = strconv.Atoi(val)
|
||||
case "events":
|
||||
info.Events = val
|
||||
case "cmd":
|
||||
@@ -5256,6 +5551,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) {
|
||||
info.LibName = val
|
||||
case "lib-ver":
|
||||
info.LibVer = val
|
||||
case "io-thread":
|
||||
info.IoThread, err = strconv.Atoi(val)
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: unexpected client info key(%s)", key)
|
||||
}
|
||||
@@ -5435,8 +5732,6 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error {
|
||||
|
||||
section := ""
|
||||
scanner := bufio.NewScanner(strings.NewReader(val))
|
||||
moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") {
|
||||
@@ -5447,6 +5742,7 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error {
|
||||
cmd.val[section] = make(map[string]string)
|
||||
} else if line != "" {
|
||||
if section == "Modules" {
|
||||
moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)
|
||||
kv := moduleRe.FindStringSubmatch(line)
|
||||
if len(kv) == 3 {
|
||||
cmd.val[section][kv[1]] = kv[2]
|
||||
@@ -5557,3 +5853,59 @@ func (cmd *MonitorCmd) Stop() {
|
||||
defer cmd.mu.Unlock()
|
||||
cmd.status = monitorStatusStop
|
||||
}
|
||||
|
||||
type VectorScoreSliceCmd struct {
|
||||
baseCmd
|
||||
|
||||
val []VectorScore
|
||||
}
|
||||
|
||||
var _ Cmder = (*VectorScoreSliceCmd)(nil)
|
||||
|
||||
func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd {
|
||||
return &VectorScoreSliceCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *VectorScoreSliceCmd) Val() []VectorScore {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *VectorScoreSliceCmd) Result() ([]VectorScore, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *VectorScoreSliceCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error {
|
||||
n, err := rd.ReadMapLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.val = make([]VectorScore, n)
|
||||
for i := 0; i < n; i++ {
|
||||
name, err := rd.ReadString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[i].Name = name
|
||||
|
||||
score, err := rd.ReadFloat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[i].Score = score
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+70
-3
@@ -81,6 +81,8 @@ func appendArg(dst []interface{}, arg interface{}) []interface{} {
|
||||
return dst
|
||||
case time.Time, time.Duration, encoding.BinaryMarshaler, net.IP:
|
||||
return append(dst, arg)
|
||||
case nil:
|
||||
return dst
|
||||
default:
|
||||
// scan struct field
|
||||
v := reflect.ValueOf(arg)
|
||||
@@ -153,6 +155,12 @@ func isEmptyValue(v reflect.Value) bool {
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
return v.IsNil()
|
||||
case reflect.Struct:
|
||||
if v.Type() == reflect.TypeOf(time.Time{}) {
|
||||
return v.IsZero()
|
||||
}
|
||||
// Only supports the struct time.Time,
|
||||
// subsequent iterations will follow the func Scan support decoder.
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -185,6 +193,7 @@ type Cmdable interface {
|
||||
ClientID(ctx context.Context) *IntCmd
|
||||
ClientUnblock(ctx context.Context, id int64) *IntCmd
|
||||
ClientUnblockWithError(ctx context.Context, id int64) *IntCmd
|
||||
ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd
|
||||
ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd
|
||||
ConfigResetStat(ctx context.Context) *StatusCmd
|
||||
ConfigSet(ctx context.Context, parameter, value string) *StatusCmd
|
||||
@@ -202,16 +211,19 @@ type Cmdable interface {
|
||||
ShutdownNoSave(ctx context.Context) *StatusCmd
|
||||
SlaveOf(ctx context.Context, host, port string) *StatusCmd
|
||||
SlowLogGet(ctx context.Context, num int64) *SlowLogCmd
|
||||
SlowLogLen(ctx context.Context) *IntCmd
|
||||
SlowLogReset(ctx context.Context) *StatusCmd
|
||||
Time(ctx context.Context) *TimeCmd
|
||||
DebugObject(ctx context.Context, key string) *StringCmd
|
||||
MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd
|
||||
Latency(ctx context.Context) *LatencyCmd
|
||||
LatencyReset(ctx context.Context, events ...interface{}) *StatusCmd
|
||||
|
||||
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
|
||||
|
||||
ACLCmdable
|
||||
BitMapCmdable
|
||||
ClusterCmdable
|
||||
GearsCmdable
|
||||
GenericCmdable
|
||||
GeoCmdable
|
||||
HashCmdable
|
||||
@@ -227,6 +239,7 @@ type Cmdable interface {
|
||||
StreamCmdable
|
||||
TimeseriesCmdable
|
||||
JSONCmdable
|
||||
VectorSetCmdable
|
||||
}
|
||||
|
||||
type StatefulCmdable interface {
|
||||
@@ -245,6 +258,7 @@ var (
|
||||
_ Cmdable = (*Tx)(nil)
|
||||
_ Cmdable = (*Ring)(nil)
|
||||
_ Cmdable = (*ClusterClient)(nil)
|
||||
_ Cmdable = (*Pipeline)(nil)
|
||||
)
|
||||
|
||||
type cmdable func(ctx context.Context, cmd Cmder) error
|
||||
@@ -331,7 +345,7 @@ func (info LibraryInfo) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hello Set the resp protocol used.
|
||||
// Hello sets the resp protocol used.
|
||||
func (c statefulCmdable) Hello(ctx context.Context,
|
||||
ver int, username, password, clientName string,
|
||||
) *MapStringInterfaceCmd {
|
||||
@@ -423,6 +437,12 @@ func (c cmdable) Ping(ctx context.Context) *StatusCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Quit(_ context.Context) *StatusCmd {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -504,6 +524,23 @@ func (c cmdable) ClientInfo(ctx context.Context) *ClientInfoCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ClientMaintNotifications enables or disables maintenance notifications for maintenance upgrades.
|
||||
// When enabled, the client will receive push notifications about Redis maintenance events.
|
||||
func (c cmdable) ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd {
|
||||
args := []interface{}{"client", "maint_notifications"}
|
||||
if enabled {
|
||||
if endpointType == "" {
|
||||
endpointType = "none"
|
||||
}
|
||||
args = append(args, "on", "moving-endpoint-type", endpointType)
|
||||
} else {
|
||||
args = append(args, "off")
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (c cmdable) ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd {
|
||||
@@ -640,6 +677,34 @@ func (c cmdable) SlowLogGet(ctx context.Context, num int64) *SlowLogCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SlowLogLen(ctx context.Context) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "slowlog", "len")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SlowLogReset(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "slowlog", "reset")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Latency(ctx context.Context) *LatencyCmd {
|
||||
cmd := NewLatencyCmd(ctx, "latency", "latest")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LatencyReset(ctx context.Context, events ...interface{}) *StatusCmd {
|
||||
args := make([]interface{}, 2+len(events))
|
||||
args[0] = "latency"
|
||||
args[1] = "reset"
|
||||
copy(args[2:], events)
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Sync(_ context.Context) {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -660,7 +725,9 @@ func (c cmdable) MemoryUsage(ctx context.Context, key string, samples ...int) *I
|
||||
args := []interface{}{"memory", "usage", key}
|
||||
if len(samples) > 0 {
|
||||
if len(samples) != 1 {
|
||||
panic("MemoryUsage expects single sample count")
|
||||
cmd := NewIntCmd(ctx)
|
||||
cmd.SetErr(errors.New("MemoryUsage expects single sample count"))
|
||||
return cmd
|
||||
}
|
||||
args = append(args, "SAMPLES", samples[0])
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
platform: linux/amd64
|
||||
container_name: redis-standalone
|
||||
environment:
|
||||
- TLS_ENABLED=yes
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=6379
|
||||
- TLS_PORT=6666
|
||||
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6666:6666 # TLS port
|
||||
volumes:
|
||||
- "./dockers/standalone:/redis/work"
|
||||
profiles:
|
||||
- standalone
|
||||
- sentinel
|
||||
- all-stack
|
||||
- all
|
||||
|
||||
osscluster:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
platform: linux/amd64
|
||||
container_name: redis-osscluster
|
||||
environment:
|
||||
- NODES=6
|
||||
- PORT=16600
|
||||
command: "--cluster-enabled yes"
|
||||
ports:
|
||||
- "16600-16605:16600-16605"
|
||||
volumes:
|
||||
- "./dockers/osscluster:/redis/work"
|
||||
profiles:
|
||||
- cluster
|
||||
- all-stack
|
||||
- all
|
||||
|
||||
sentinel-cluster:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
platform: linux/amd64
|
||||
container_name: redis-sentinel-cluster
|
||||
network_mode: "host"
|
||||
environment:
|
||||
- NODES=3
|
||||
- TLS_ENABLED=yes
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=9121
|
||||
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
||||
#ports:
|
||||
# - "9121-9123:9121-9123"
|
||||
volumes:
|
||||
- "./dockers/sentinel-cluster:/redis/work"
|
||||
profiles:
|
||||
- sentinel
|
||||
- all-stack
|
||||
- all
|
||||
|
||||
sentinel:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
platform: linux/amd64
|
||||
container_name: redis-sentinel
|
||||
depends_on:
|
||||
- sentinel-cluster
|
||||
environment:
|
||||
- NODES=3
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=26379
|
||||
command: ${REDIS_EXTRA_ARGS:---sentinel}
|
||||
network_mode: "host"
|
||||
#ports:
|
||||
# - 26379:26379
|
||||
# - 26380:26380
|
||||
# - 26381:26381
|
||||
volumes:
|
||||
- "./dockers/sentinel.conf:/redis/config-default/redis.conf"
|
||||
- "./dockers/sentinel:/redis/work"
|
||||
profiles:
|
||||
- sentinel
|
||||
- all-stack
|
||||
- all
|
||||
|
||||
ring-cluster:
|
||||
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
|
||||
platform: linux/amd64
|
||||
container_name: redis-ring-cluster
|
||||
environment:
|
||||
- NODES=3
|
||||
- TLS_ENABLED=yes
|
||||
- REDIS_CLUSTER=no
|
||||
- PORT=6390
|
||||
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
||||
ports:
|
||||
- 6390:6390
|
||||
- 6391:6391
|
||||
- 6392:6392
|
||||
volumes:
|
||||
- "./dockers/ring:/redis/work"
|
||||
profiles:
|
||||
- ring
|
||||
- cluster
|
||||
- all-stack
|
||||
- all
|
||||
+226
-36
@@ -15,6 +15,19 @@ import (
|
||||
// ErrClosed performs any operation on the closed client will return this error.
|
||||
var ErrClosed = pool.ErrClosed
|
||||
|
||||
// ErrPoolExhausted is returned from a pool connection method
|
||||
// when the maximum number of database connections in the pool has been reached.
|
||||
var ErrPoolExhausted = pool.ErrPoolExhausted
|
||||
|
||||
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
|
||||
var ErrPoolTimeout = pool.ErrPoolTimeout
|
||||
|
||||
// ErrCrossSlot is returned when keys are used in the same Redis command and
|
||||
// the keys are not in the same hash slot. This error is returned by Redis
|
||||
// Cluster and will be returned by the client when TxPipeline or TxPipelined
|
||||
// is used on a ClusterClient with keys in different slots.
|
||||
var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot")
|
||||
|
||||
// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.
|
||||
func HasErrorPrefix(err error, prefix string) bool {
|
||||
var rErr Error
|
||||
@@ -38,23 +51,83 @@ type Error interface {
|
||||
|
||||
var _ Error = proto.RedisError("")
|
||||
|
||||
func isContextError(err error) bool {
|
||||
// Check for wrapped context errors using errors.Is
|
||||
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
// isTimeoutError checks if an error is a timeout error, even if wrapped.
|
||||
// Returns (isTimeout, shouldRetryOnTimeout) where:
|
||||
// - isTimeout: true if the error is any kind of timeout error
|
||||
// - shouldRetryOnTimeout: true if Timeout() method returns true
|
||||
func isTimeoutError(err error) (isTimeout bool, hasTimeoutFlag bool) {
|
||||
// Check for timeoutError interface (works with wrapped errors)
|
||||
var te timeoutError
|
||||
if errors.As(err, &te) {
|
||||
return true, te.Timeout()
|
||||
}
|
||||
|
||||
// Check for net.Error specifically (common case for network timeouts)
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) {
|
||||
return true, netErr.Timeout()
|
||||
}
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
func shouldRetry(err error, retryTimeout bool) bool {
|
||||
switch err {
|
||||
case io.EOF, io.ErrUnexpectedEOF:
|
||||
return true
|
||||
case nil, context.Canceled, context.DeadlineExceeded:
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if v, ok := err.(timeoutError); ok {
|
||||
if v.Timeout() {
|
||||
// Check for EOF errors (works with wrapped errors)
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for context errors (works with wrapped errors)
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for pool timeout (works with wrapped errors)
|
||||
if errors.Is(err, pool.ErrPoolTimeout) {
|
||||
// connection pool timeout, increase retries. #3289
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for timeout errors (works with wrapped errors)
|
||||
if isTimeout, hasTimeoutFlag := isTimeoutError(err); isTimeout {
|
||||
if hasTimeoutFlag {
|
||||
return retryTimeout
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for typed Redis errors using errors.As (works with wrapped errors)
|
||||
if proto.IsMaxClientsError(err) {
|
||||
return true
|
||||
}
|
||||
if proto.IsLoadingError(err) {
|
||||
return true
|
||||
}
|
||||
if proto.IsReadOnlyError(err) {
|
||||
return true
|
||||
}
|
||||
if proto.IsMasterDownError(err) {
|
||||
return true
|
||||
}
|
||||
if proto.IsClusterDownError(err) {
|
||||
return true
|
||||
}
|
||||
if proto.IsTryAgainError(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback to string checking for backward compatibility with plain errors
|
||||
s := err.Error()
|
||||
if s == "ERR max number of clients reached" {
|
||||
if strings.HasPrefix(s, "ERR max number of clients reached") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(s, "LOADING ") {
|
||||
@@ -69,20 +142,36 @@ func shouldRetry(err error, retryTimeout bool) bool {
|
||||
if strings.HasPrefix(s, "TRYAGAIN ") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(s, "MASTERDOWN ") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isRedisError(err error) bool {
|
||||
_, ok := err.(proto.RedisError)
|
||||
return ok
|
||||
// Check if error implements the Error interface (works with wrapped errors)
|
||||
var redisErr Error
|
||||
if errors.As(err, &redisErr) {
|
||||
return true
|
||||
}
|
||||
// Also check for proto.RedisError specifically
|
||||
var protoRedisErr proto.RedisError
|
||||
return errors.As(err, &protoRedisErr)
|
||||
}
|
||||
|
||||
func isBadConn(err error, allowTimeout bool, addr string) bool {
|
||||
switch err {
|
||||
case nil:
|
||||
if err == nil {
|
||||
return false
|
||||
case context.Canceled, context.DeadlineExceeded:
|
||||
}
|
||||
|
||||
// Check for context errors (works with wrapped errors)
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for pool timeout errors (works with wrapped errors)
|
||||
if errors.Is(err, pool.ErrConnUnusableTimeout) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -103,7 +192,9 @@ func isBadConn(err error, allowTimeout bool, addr string) bool {
|
||||
}
|
||||
|
||||
if allowTimeout {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
// Check for network timeout errors (works with wrapped errors)
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -112,44 +203,143 @@ func isBadConn(err error, allowTimeout bool, addr string) bool {
|
||||
}
|
||||
|
||||
func isMovedError(err error) (moved bool, ask bool, addr string) {
|
||||
if !isRedisError(err) {
|
||||
return
|
||||
// Check for typed MovedError
|
||||
if movedErr, ok := proto.IsMovedError(err); ok {
|
||||
addr = movedErr.Addr()
|
||||
addr = internal.GetAddr(addr)
|
||||
return true, false, addr
|
||||
}
|
||||
|
||||
// Check for typed AskError
|
||||
if askErr, ok := proto.IsAskError(err); ok {
|
||||
addr = askErr.Addr()
|
||||
addr = internal.GetAddr(addr)
|
||||
return false, true, addr
|
||||
}
|
||||
|
||||
// Fallback to string checking for backward compatibility
|
||||
s := err.Error()
|
||||
switch {
|
||||
case strings.HasPrefix(s, "MOVED "):
|
||||
moved = true
|
||||
case strings.HasPrefix(s, "ASK "):
|
||||
ask = true
|
||||
default:
|
||||
return
|
||||
if strings.HasPrefix(s, "MOVED ") {
|
||||
// Parse: MOVED 3999 127.0.0.1:6381
|
||||
parts := strings.Split(s, " ")
|
||||
if len(parts) == 3 {
|
||||
addr = internal.GetAddr(parts[2])
|
||||
return true, false, addr
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(s, "ASK ") {
|
||||
// Parse: ASK 3999 127.0.0.1:6381
|
||||
parts := strings.Split(s, " ")
|
||||
if len(parts) == 3 {
|
||||
addr = internal.GetAddr(parts[2])
|
||||
return false, true, addr
|
||||
}
|
||||
}
|
||||
|
||||
ind := strings.LastIndex(s, " ")
|
||||
if ind == -1 {
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
addr = s[ind+1:]
|
||||
addr = internal.GetAddr(addr)
|
||||
return
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
func isLoadingError(err error) bool {
|
||||
return strings.HasPrefix(err.Error(), "LOADING ")
|
||||
return proto.IsLoadingError(err)
|
||||
}
|
||||
|
||||
func isReadOnlyError(err error) bool {
|
||||
return strings.HasPrefix(err.Error(), "READONLY ")
|
||||
return proto.IsReadOnlyError(err)
|
||||
}
|
||||
|
||||
func isMovedSameConnAddr(err error, addr string) bool {
|
||||
redisError := err.Error()
|
||||
if !strings.HasPrefix(redisError, "MOVED ") {
|
||||
return false
|
||||
if movedErr, ok := proto.IsMovedError(err); ok {
|
||||
return strings.HasSuffix(movedErr.Addr(), addr)
|
||||
}
|
||||
return strings.HasSuffix(redisError, " "+addr)
|
||||
return false
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Typed error checking functions for public use.
|
||||
// These functions work correctly even when errors are wrapped in hooks.
|
||||
|
||||
// IsLoadingError checks if an error is a Redis LOADING error, even if wrapped.
|
||||
// LOADING errors occur when Redis is loading the dataset in memory.
|
||||
func IsLoadingError(err error) bool {
|
||||
return proto.IsLoadingError(err)
|
||||
}
|
||||
|
||||
// IsReadOnlyError checks if an error is a Redis READONLY error, even if wrapped.
|
||||
// READONLY errors occur when trying to write to a read-only replica.
|
||||
func IsReadOnlyError(err error) bool {
|
||||
return proto.IsReadOnlyError(err)
|
||||
}
|
||||
|
||||
// IsClusterDownError checks if an error is a Redis CLUSTERDOWN error, even if wrapped.
|
||||
// CLUSTERDOWN errors occur when the cluster is down.
|
||||
func IsClusterDownError(err error) bool {
|
||||
return proto.IsClusterDownError(err)
|
||||
}
|
||||
|
||||
// IsTryAgainError checks if an error is a Redis TRYAGAIN error, even if wrapped.
|
||||
// TRYAGAIN errors occur when a command cannot be processed and should be retried.
|
||||
func IsTryAgainError(err error) bool {
|
||||
return proto.IsTryAgainError(err)
|
||||
}
|
||||
|
||||
// IsMasterDownError checks if an error is a Redis MASTERDOWN error, even if wrapped.
|
||||
// MASTERDOWN errors occur when the master is down.
|
||||
func IsMasterDownError(err error) bool {
|
||||
return proto.IsMasterDownError(err)
|
||||
}
|
||||
|
||||
// IsMaxClientsError checks if an error is a Redis max clients error, even if wrapped.
|
||||
// This error occurs when the maximum number of clients has been reached.
|
||||
func IsMaxClientsError(err error) bool {
|
||||
return proto.IsMaxClientsError(err)
|
||||
}
|
||||
|
||||
// IsMovedError checks if an error is a Redis MOVED error, even if wrapped.
|
||||
// MOVED errors occur in cluster mode when a key has been moved to a different node.
|
||||
// Returns the address of the node where the key has been moved and a boolean indicating if it's a MOVED error.
|
||||
func IsMovedError(err error) (addr string, ok bool) {
|
||||
if movedErr, isMovedErr := proto.IsMovedError(err); isMovedErr {
|
||||
return movedErr.Addr(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IsAskError checks if an error is a Redis ASK error, even if wrapped.
|
||||
// ASK errors occur in cluster mode when a key is being migrated and the client should ask another node.
|
||||
// Returns the address of the node to ask and a boolean indicating if it's an ASK error.
|
||||
func IsAskError(err error) (addr string, ok bool) {
|
||||
if askErr, isAskErr := proto.IsAskError(err); isAskErr {
|
||||
return askErr.Addr(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IsAuthError checks if an error is a Redis authentication error, even if wrapped.
|
||||
// Authentication errors occur when:
|
||||
// - NOAUTH: Redis requires authentication but none was provided
|
||||
// - WRONGPASS: Redis authentication failed due to incorrect password
|
||||
// - unauthenticated: Error returned when password changed
|
||||
func IsAuthError(err error) bool {
|
||||
return proto.IsAuthError(err)
|
||||
}
|
||||
|
||||
// IsPermissionError checks if an error is a Redis permission error, even if wrapped.
|
||||
// Permission errors (NOPERM) occur when a user does not have permission to execute a command.
|
||||
func IsPermissionError(err error) bool {
|
||||
return proto.IsPermissionError(err)
|
||||
}
|
||||
|
||||
// IsExecAbortError checks if an error is a Redis EXECABORT error, even if wrapped.
|
||||
// EXECABORT errors occur when a transaction is aborted.
|
||||
func IsExecAbortError(err error) bool {
|
||||
return proto.IsExecAbortError(err)
|
||||
}
|
||||
|
||||
// IsOOMError checks if an error is a Redis OOM (Out Of Memory) error, even if wrapped.
|
||||
// OOM errors occur when Redis is out of memory.
|
||||
func IsOOMError(err error) bool {
|
||||
return proto.IsOOMError(err)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GearsCmdable interface {
|
||||
TFunctionLoad(ctx context.Context, lib string) *StatusCmd
|
||||
TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd
|
||||
TFunctionDelete(ctx context.Context, libName string) *StatusCmd
|
||||
TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd
|
||||
TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd
|
||||
TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
|
||||
TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
|
||||
TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
|
||||
TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
|
||||
}
|
||||
|
||||
type TFunctionLoadOptions struct {
|
||||
Replace bool
|
||||
Config string
|
||||
}
|
||||
|
||||
type TFunctionListOptions struct {
|
||||
Withcode bool
|
||||
Verbose int
|
||||
Library string
|
||||
}
|
||||
|
||||
type TFCallOptions struct {
|
||||
Keys []string
|
||||
Arguments []string
|
||||
}
|
||||
|
||||
// TFunctionLoad - load a new JavaScript library into Redis.
|
||||
// For more information - https://redis.io/commands/tfunction-load/
|
||||
func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd {
|
||||
args := []interface{}{"TFUNCTION", "LOAD", lib}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd {
|
||||
args := []interface{}{"TFUNCTION", "LOAD"}
|
||||
if options != nil {
|
||||
if options.Replace {
|
||||
args = append(args, "REPLACE")
|
||||
}
|
||||
if options.Config != "" {
|
||||
args = append(args, "CONFIG", options.Config)
|
||||
}
|
||||
}
|
||||
args = append(args, lib)
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFunctionDelete - delete a JavaScript library from Redis.
|
||||
// For more information - https://redis.io/commands/tfunction-delete/
|
||||
func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd {
|
||||
args := []interface{}{"TFUNCTION", "DELETE", libName}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFunctionList - list the functions with additional information about each function.
|
||||
// For more information - https://redis.io/commands/tfunction-list/
|
||||
func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd {
|
||||
args := []interface{}{"TFUNCTION", "LIST"}
|
||||
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd {
|
||||
args := []interface{}{"TFUNCTION", "LIST"}
|
||||
if options != nil {
|
||||
if options.Withcode {
|
||||
args = append(args, "WITHCODE")
|
||||
}
|
||||
if options.Verbose != 0 {
|
||||
v := strings.Repeat("v", options.Verbose)
|
||||
args = append(args, v)
|
||||
}
|
||||
if options.Library != "" {
|
||||
args = append(args, "LIBRARY", options.Library)
|
||||
}
|
||||
}
|
||||
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFCall - invoke a function.
|
||||
// For more information - https://redis.io/commands/tfcall/
|
||||
func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
|
||||
lf := libName + "." + funcName
|
||||
args := []interface{}{"TFCALL", lf, numKeys}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
|
||||
lf := libName + "." + funcName
|
||||
args := []interface{}{"TFCALL", lf, numKeys}
|
||||
if options != nil {
|
||||
for _, key := range options.Keys {
|
||||
args = append(args, key)
|
||||
}
|
||||
for _, key := range options.Arguments {
|
||||
args = append(args, key)
|
||||
}
|
||||
}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFCallASYNC - invoke an asynchronous JavaScript function (coroutine).
|
||||
// For more information - https://redis.io/commands/TFCallASYNC/
|
||||
func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
|
||||
lf := fmt.Sprintf("%s.%s", libName, funcName)
|
||||
args := []interface{}{"TFCALLASYNC", lf, numKeys}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
|
||||
lf := fmt.Sprintf("%s.%s", libName, funcName)
|
||||
args := []interface{}{"TFCALLASYNC", lf, numKeys}
|
||||
if options != nil {
|
||||
for _, key := range options.Keys {
|
||||
args = append(args, key)
|
||||
}
|
||||
for _, key := range options.Arguments {
|
||||
args = append(args, key)
|
||||
}
|
||||
}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+8
@@ -3,6 +3,8 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/hashtag"
|
||||
)
|
||||
|
||||
type GenericCmdable interface {
|
||||
@@ -363,6 +365,9 @@ func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count in
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
if hashtag.Present(match) {
|
||||
cmd.SetFirstKeyPos(3)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
@@ -379,6 +384,9 @@ func (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, coun
|
||||
args = append(args, "type", keyType)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
if hashtag.Present(match) {
|
||||
cmd.SetFirstKeyPos(3)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
+180
-11
@@ -3,6 +3,8 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/hashtag"
|
||||
)
|
||||
|
||||
type HashCmdable interface {
|
||||
@@ -10,6 +12,9 @@ type HashCmdable interface {
|
||||
HExists(ctx context.Context, key, field string) *BoolCmd
|
||||
HGet(ctx context.Context, key, field string) *StringCmd
|
||||
HGetAll(ctx context.Context, key string) *MapStringStringCmd
|
||||
HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd
|
||||
HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd
|
||||
HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd
|
||||
HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd
|
||||
HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd
|
||||
HKeys(ctx context.Context, key string) *StringSliceCmd
|
||||
@@ -17,12 +22,15 @@ type HashCmdable interface {
|
||||
HMGet(ctx context.Context, key string, fields ...string) *SliceCmd
|
||||
HSet(ctx context.Context, key string, values ...interface{}) *IntCmd
|
||||
HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd
|
||||
HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd
|
||||
HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd
|
||||
HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd
|
||||
HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
|
||||
HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
|
||||
HVals(ctx context.Context, key string) *StringSliceCmd
|
||||
HRandField(ctx context.Context, key string, count int) *StringSliceCmd
|
||||
HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd
|
||||
HStrLen(ctx context.Context, key, field string) *IntCmd
|
||||
HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd
|
||||
HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd
|
||||
HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd
|
||||
@@ -108,16 +116,16 @@ func (c cmdable) HMGet(ctx context.Context, key string, fields ...string) *Slice
|
||||
|
||||
// HSet accepts values in following formats:
|
||||
//
|
||||
// - HSet("myhash", "key1", "value1", "key2", "value2")
|
||||
// - HSet(ctx, "myhash", "key1", "value1", "key2", "value2")
|
||||
//
|
||||
// - HSet("myhash", []string{"key1", "value1", "key2", "value2"})
|
||||
// - HSet(ctx, "myhash", []string{"key1", "value1", "key2", "value2"})
|
||||
//
|
||||
// - HSet("myhash", map[string]interface{}{"key1": "value1", "key2": "value2"})
|
||||
// - HSet(ctx, "myhash", map[string]interface{}{"key1": "value1", "key2": "value2"})
|
||||
//
|
||||
// Playing struct With "redis" tag.
|
||||
// type MyHash struct { Key1 string `redis:"key1"`; Key2 int `redis:"key2"` }
|
||||
//
|
||||
// - HSet("myhash", MyHash{"value1", "value2"}) Warn: redis-server >= 4.0
|
||||
// - HSet(ctx, "myhash", MyHash{"value1", "value2"}) Warn: redis-server >= 4.0
|
||||
//
|
||||
// For struct, can be a structure pointer type, we only parse the field whose tag is redis.
|
||||
// if you don't want the field to be read, you can use the `redis:"-"` flag to ignore it,
|
||||
@@ -186,10 +194,18 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
if hashtag.Present(match) {
|
||||
cmd.SetFirstKeyPos(4)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HStrLen(ctx context.Context, key, field string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "hstrlen", key, field)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
|
||||
args := []interface{}{"hscan", key, cursor}
|
||||
if match != "" {
|
||||
@@ -200,6 +216,9 @@ func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, m
|
||||
}
|
||||
args = append(args, "novalues")
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
if hashtag.Present(match) {
|
||||
cmd.SetFirstKeyPos(4)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
@@ -213,7 +232,10 @@ type HExpireArgs struct {
|
||||
|
||||
// HExpire - Sets the expiration time for specified fields in a hash in seconds.
|
||||
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
|
||||
// For more information - https://redis.io/commands/hexpire/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HEXPIRE Documentation].
|
||||
//
|
||||
// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/
|
||||
func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration), "FIELDS", len(fields)}
|
||||
|
||||
@@ -228,7 +250,10 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati
|
||||
// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds.
|
||||
// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
|
||||
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
|
||||
// For more information - https://redis.io/commands/hexpire/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HEXPIRE Documentation].
|
||||
//
|
||||
// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/
|
||||
func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration)}
|
||||
|
||||
@@ -257,7 +282,10 @@ func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration tim
|
||||
// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds.
|
||||
// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields.
|
||||
// The command modifies the standard time.Duration to milliseconds for the Redis command.
|
||||
// For more information - https://redis.io/commands/hpexpire/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HPEXPIRE Documentation].
|
||||
//
|
||||
// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/
|
||||
func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)}
|
||||
|
||||
@@ -269,6 +297,13 @@ func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Durat
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HPExpireWithArgs - Sets the expiration time for specified fields in a hash in milliseconds.
|
||||
// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
|
||||
// The command constructs an argument list starting with "HPEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HPEXPIRE Documentation].
|
||||
//
|
||||
// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/
|
||||
func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)}
|
||||
|
||||
@@ -297,7 +332,10 @@ func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration ti
|
||||
// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds.
|
||||
// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields.
|
||||
// The command sets absolute expiration times based on the UNIX timestamp provided.
|
||||
// For more information - https://redis.io/commands/hexpireat/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HExpireAt Documentation].
|
||||
//
|
||||
// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/
|
||||
func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
|
||||
|
||||
args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)}
|
||||
@@ -337,7 +375,10 @@ func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time
|
||||
|
||||
// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds.
|
||||
// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds.
|
||||
// For more information - https://redis.io/commands/hpexpireat/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HExpireAt Documentation].
|
||||
//
|
||||
// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/
|
||||
func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)}
|
||||
|
||||
@@ -377,7 +418,10 @@ func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Tim
|
||||
// HPersist - Removes the expiration time from specified fields in a hash.
|
||||
// Accepts a key and the fields themselves.
|
||||
// This command ensures that each field specified will have its expiration removed if present.
|
||||
// For more information - https://redis.io/commands/hpersist/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HPersist Documentation].
|
||||
//
|
||||
// [HPersist Documentation]: https://redis.io/commands/hpersist/
|
||||
func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)}
|
||||
|
||||
@@ -392,6 +436,10 @@ func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *In
|
||||
// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds.
|
||||
// Requires a key and the fields themselves to fetch their expiration timestamps.
|
||||
// This command returns the expiration times for each field or error/status codes for each field as specified.
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HExpireTime Documentation].
|
||||
//
|
||||
// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/
|
||||
// For more information - https://redis.io/commands/hexpiretime/
|
||||
func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)}
|
||||
@@ -407,6 +455,10 @@ func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string)
|
||||
// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds.
|
||||
// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters.
|
||||
// Provides the expiration timestamp for each field in milliseconds.
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HExpireTime Documentation].
|
||||
//
|
||||
// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/
|
||||
// For more information - https://redis.io/commands/hexpiretime/
|
||||
func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)}
|
||||
@@ -422,7 +474,10 @@ func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string)
|
||||
// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds.
|
||||
// Requires a key and the fields themselves. It returns the TTL for each specified field.
|
||||
// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate.
|
||||
// For more information - https://redis.io/commands/httl/
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HTTL Documentation].
|
||||
//
|
||||
// [HTTL Documentation]: https://redis.io/commands/httl/
|
||||
func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HTTL", key, "FIELDS", len(fields)}
|
||||
|
||||
@@ -437,6 +492,10 @@ func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSli
|
||||
// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds.
|
||||
// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields.
|
||||
// This command provides the TTL in milliseconds for each field or returns error/status codes as needed.
|
||||
// Available since Redis 7.4 CE.
|
||||
// For more information refer to [HPTTL Documentation].
|
||||
//
|
||||
// [HPTTL Documentation]: https://redis.io/commands/hpttl/
|
||||
// For more information - https://redis.io/commands/hpttl/
|
||||
func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
|
||||
args := []interface{}{"HPTTL", key, "FIELDS", len(fields)}
|
||||
@@ -448,3 +507,113 @@ func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSl
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd {
|
||||
args := []interface{}{"HGETDEL", key, "FIELDS", len(fields)}
|
||||
for _, field := range fields {
|
||||
args = append(args, field)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd {
|
||||
args := []interface{}{"HGETEX", key, "FIELDS", len(fields)}
|
||||
for _, field := range fields {
|
||||
args = append(args, field)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HGetEXExpirationType represents an expiration option for the HGETEX command.
|
||||
type HGetEXExpirationType string
|
||||
|
||||
const (
|
||||
HGetEXExpirationEX HGetEXExpirationType = "EX"
|
||||
HGetEXExpirationPX HGetEXExpirationType = "PX"
|
||||
HGetEXExpirationEXAT HGetEXExpirationType = "EXAT"
|
||||
HGetEXExpirationPXAT HGetEXExpirationType = "PXAT"
|
||||
HGetEXExpirationPERSIST HGetEXExpirationType = "PERSIST"
|
||||
)
|
||||
|
||||
type HGetEXOptions struct {
|
||||
ExpirationType HGetEXExpirationType
|
||||
ExpirationVal int64
|
||||
}
|
||||
|
||||
func (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd {
|
||||
args := []interface{}{"HGETEX", key}
|
||||
if options.ExpirationType != "" {
|
||||
args = append(args, string(options.ExpirationType))
|
||||
if options.ExpirationType != HGetEXExpirationPERSIST {
|
||||
args = append(args, options.ExpirationVal)
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, "FIELDS", len(fields))
|
||||
for _, field := range fields {
|
||||
args = append(args, field)
|
||||
}
|
||||
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type HSetEXCondition string
|
||||
|
||||
const (
|
||||
HSetEXFNX HSetEXCondition = "FNX" // Only set the fields if none of them already exist.
|
||||
HSetEXFXX HSetEXCondition = "FXX" // Only set the fields if all already exist.
|
||||
)
|
||||
|
||||
type HSetEXExpirationType string
|
||||
|
||||
const (
|
||||
HSetEXExpirationEX HSetEXExpirationType = "EX"
|
||||
HSetEXExpirationPX HSetEXExpirationType = "PX"
|
||||
HSetEXExpirationEXAT HSetEXExpirationType = "EXAT"
|
||||
HSetEXExpirationPXAT HSetEXExpirationType = "PXAT"
|
||||
HSetEXExpirationKEEPTTL HSetEXExpirationType = "KEEPTTL"
|
||||
)
|
||||
|
||||
type HSetEXOptions struct {
|
||||
Condition HSetEXCondition
|
||||
ExpirationType HSetEXExpirationType
|
||||
ExpirationVal int64
|
||||
}
|
||||
|
||||
func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd {
|
||||
args := []interface{}{"HSETEX", key, "FIELDS", len(fieldsAndValues) / 2}
|
||||
for _, field := range fieldsAndValues {
|
||||
args = append(args, field)
|
||||
}
|
||||
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd {
|
||||
args := []interface{}{"HSETEX", key}
|
||||
if options.Condition != "" {
|
||||
args = append(args, string(options.Condition))
|
||||
}
|
||||
if options.ExpirationType != "" {
|
||||
args = append(args, string(options.ExpirationType))
|
||||
if options.ExpirationType != HSetEXExpirationKEEPTTL {
|
||||
args = append(args, options.ExpirationVal)
|
||||
}
|
||||
}
|
||||
args = append(args, "FIELDS", len(fieldsAndValues)/2)
|
||||
for _, field := range fieldsAndValues {
|
||||
args = append(args, field)
|
||||
}
|
||||
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
Generated
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"github.com/redis/go-redis/v9/auth"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
)
|
||||
|
||||
// ConnReAuthCredentialsListener is a credentials listener for a specific connection
|
||||
// that triggers re-authentication when credentials change.
|
||||
//
|
||||
// This listener implements the auth.CredentialsListener interface and is subscribed
|
||||
// to a StreamingCredentialsProvider. When new credentials are received via OnNext,
|
||||
// it marks the connection for re-authentication through the manager.
|
||||
//
|
||||
// The re-authentication is always performed asynchronously to avoid blocking the
|
||||
// credentials provider and to prevent potential deadlocks with the pool semaphore.
|
||||
// The actual re-auth happens when the connection is returned to the pool in an idle state.
|
||||
//
|
||||
// Lifecycle:
|
||||
// - Created during connection initialization via Manager.Listener()
|
||||
// - Subscribed to the StreamingCredentialsProvider
|
||||
// - Receives credential updates via OnNext()
|
||||
// - Cleaned up when connection is removed from pool via Manager.RemoveListener()
|
||||
type ConnReAuthCredentialsListener struct {
|
||||
// reAuth is the function to re-authenticate the connection with new credentials
|
||||
reAuth func(conn *pool.Conn, credentials auth.Credentials) error
|
||||
|
||||
// onErr is the function to call when re-authentication or acquisition fails
|
||||
onErr func(conn *pool.Conn, err error)
|
||||
|
||||
// conn is the connection this listener is associated with
|
||||
conn *pool.Conn
|
||||
|
||||
// manager is the streaming credentials manager for coordinating re-auth
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
// OnNext is called when new credentials are received from the StreamingCredentialsProvider.
|
||||
//
|
||||
// This method marks the connection for asynchronous re-authentication. The actual
|
||||
// re-authentication happens in the background when the connection is returned to the
|
||||
// pool and is in an idle state.
|
||||
//
|
||||
// Asynchronous re-auth is used to:
|
||||
// - Avoid blocking the credentials provider's notification goroutine
|
||||
// - Prevent deadlocks with the pool's semaphore (especially with small pool sizes)
|
||||
// - Ensure re-auth happens when the connection is safe to use (not processing commands)
|
||||
//
|
||||
// The reAuthFn callback receives:
|
||||
// - nil if the connection was successfully acquired for re-auth
|
||||
// - error if acquisition timed out or failed
|
||||
//
|
||||
// Thread-safe: Called by the credentials provider's notification goroutine.
|
||||
func (c *ConnReAuthCredentialsListener) OnNext(credentials auth.Credentials) {
|
||||
if c.conn == nil || c.conn.IsClosed() || c.manager == nil || c.reAuth == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Always use async reauth to avoid complex pool semaphore issues
|
||||
// The synchronous path can cause deadlocks in the pool's semaphore mechanism
|
||||
// when called from the Subscribe goroutine, especially with small pool sizes.
|
||||
// The connection pool hook will re-authenticate the connection when it is
|
||||
// returned to the pool in a clean, idle state.
|
||||
c.manager.MarkForReAuth(c.conn, func(err error) {
|
||||
// err is from connection acquisition (timeout, etc.)
|
||||
if err != nil {
|
||||
// Log the error
|
||||
c.OnError(err)
|
||||
return
|
||||
}
|
||||
// err is from reauth command execution
|
||||
err = c.reAuth(c.conn, credentials)
|
||||
if err != nil {
|
||||
// Log the error
|
||||
c.OnError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OnError is called when an error occurs during credential streaming or re-authentication.
|
||||
//
|
||||
// This method can be called from:
|
||||
// - The StreamingCredentialsProvider when there's an error in the credentials stream
|
||||
// - The re-auth process when connection acquisition times out
|
||||
// - The re-auth process when the AUTH command fails
|
||||
//
|
||||
// The error is delegated to the onErr callback provided during listener creation.
|
||||
//
|
||||
// Thread-safe: Can be called from multiple goroutines (provider, re-auth worker).
|
||||
func (c *ConnReAuthCredentialsListener) OnError(err error) {
|
||||
if c.onErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.onErr(c.conn, err)
|
||||
}
|
||||
|
||||
// Ensure ConnReAuthCredentialsListener implements the CredentialsListener interface.
|
||||
var _ auth.CredentialsListener = (*ConnReAuthCredentialsListener)(nil)
|
||||
Generated
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/redis/go-redis/v9/auth"
|
||||
)
|
||||
|
||||
// CredentialsListeners is a thread-safe collection of credentials listeners
|
||||
// indexed by connection ID.
|
||||
//
|
||||
// This collection is used by the Manager to maintain a registry of listeners
|
||||
// for each connection in the pool. Listeners are reused when connections are
|
||||
// reinitialized (e.g., after a handoff) to avoid creating duplicate subscriptions
|
||||
// to the StreamingCredentialsProvider.
|
||||
//
|
||||
// The collection supports concurrent access from multiple goroutines during
|
||||
// connection initialization, credential updates, and connection removal.
|
||||
type CredentialsListeners struct {
|
||||
// listeners maps connection ID to credentials listener
|
||||
listeners map[uint64]auth.CredentialsListener
|
||||
|
||||
// lock protects concurrent access to the listeners map
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCredentialsListeners creates a new thread-safe credentials listeners collection.
|
||||
func NewCredentialsListeners() *CredentialsListeners {
|
||||
return &CredentialsListeners{
|
||||
listeners: make(map[uint64]auth.CredentialsListener),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds or updates a credentials listener for a connection.
|
||||
//
|
||||
// If a listener already exists for the connection ID, it is replaced.
|
||||
// This is safe because the old listener should have been unsubscribed
|
||||
// before the connection was reinitialized.
|
||||
//
|
||||
// Thread-safe: Can be called concurrently from multiple goroutines.
|
||||
func (c *CredentialsListeners) Add(connID uint64, listener auth.CredentialsListener) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.listeners == nil {
|
||||
c.listeners = make(map[uint64]auth.CredentialsListener)
|
||||
}
|
||||
c.listeners[connID] = listener
|
||||
}
|
||||
|
||||
// Get retrieves the credentials listener for a connection.
|
||||
//
|
||||
// Returns:
|
||||
// - listener: The credentials listener for the connection, or nil if not found
|
||||
// - ok: true if a listener exists for the connection ID, false otherwise
|
||||
//
|
||||
// Thread-safe: Can be called concurrently from multiple goroutines.
|
||||
func (c *CredentialsListeners) Get(connID uint64) (auth.CredentialsListener, bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
if len(c.listeners) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
listener, ok := c.listeners[connID]
|
||||
return listener, ok
|
||||
}
|
||||
|
||||
// Remove removes the credentials listener for a connection.
|
||||
//
|
||||
// This is called when a connection is removed from the pool to prevent
|
||||
// memory leaks. If no listener exists for the connection ID, this is a no-op.
|
||||
//
|
||||
// Thread-safe: Can be called concurrently from multiple goroutines.
|
||||
func (c *CredentialsListeners) Remove(connID uint64) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.listeners, connID)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/auth"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
)
|
||||
|
||||
// Manager coordinates streaming credentials and re-authentication for a connection pool.
|
||||
//
|
||||
// The manager is responsible for:
|
||||
// - Creating and managing per-connection credentials listeners
|
||||
// - Providing the pool hook for re-authentication
|
||||
// - Coordinating between credentials updates and pool operations
|
||||
//
|
||||
// When credentials change via a StreamingCredentialsProvider:
|
||||
// 1. The credentials listener (ConnReAuthCredentialsListener) receives the update
|
||||
// 2. It calls MarkForReAuth on the manager
|
||||
// 3. The manager delegates to the pool hook
|
||||
// 4. The pool hook schedules background re-authentication
|
||||
//
|
||||
// The manager maintains a registry of credentials listeners indexed by connection ID,
|
||||
// allowing listener reuse when connections are reinitialized (e.g., after handoff).
|
||||
type Manager struct {
|
||||
// credentialsListeners maps connection ID to credentials listener
|
||||
credentialsListeners *CredentialsListeners
|
||||
|
||||
// pool is the connection pool being managed
|
||||
pool pool.Pooler
|
||||
|
||||
// poolHookRef is the re-authentication pool hook
|
||||
poolHookRef *ReAuthPoolHook
|
||||
}
|
||||
|
||||
// NewManager creates a new streaming credentials manager.
|
||||
//
|
||||
// Parameters:
|
||||
// - pl: The connection pool to manage
|
||||
// - reAuthTimeout: Maximum time to wait for acquiring a connection for re-authentication
|
||||
//
|
||||
// The manager creates a ReAuthPoolHook sized to match the pool size, ensuring that
|
||||
// re-auth operations don't exhaust the connection pool.
|
||||
func NewManager(pl pool.Pooler, reAuthTimeout time.Duration) *Manager {
|
||||
m := &Manager{
|
||||
pool: pl,
|
||||
poolHookRef: NewReAuthPoolHook(pl.Size(), reAuthTimeout),
|
||||
credentialsListeners: NewCredentialsListeners(),
|
||||
}
|
||||
m.poolHookRef.manager = m
|
||||
return m
|
||||
}
|
||||
|
||||
// PoolHook returns the pool hook for re-authentication.
|
||||
//
|
||||
// This hook should be registered with the connection pool to enable
|
||||
// automatic re-authentication when credentials change.
|
||||
func (m *Manager) PoolHook() pool.PoolHook {
|
||||
return m.poolHookRef
|
||||
}
|
||||
|
||||
// Listener returns or creates a credentials listener for a connection.
|
||||
//
|
||||
// This method is called during connection initialization to set up the
|
||||
// credentials listener. If a listener already exists for the connection ID
|
||||
// (e.g., after a handoff), it is reused.
|
||||
//
|
||||
// Parameters:
|
||||
// - poolCn: The connection to create/get a listener for
|
||||
// - reAuth: Function to re-authenticate the connection with new credentials
|
||||
// - onErr: Function to call when re-authentication fails
|
||||
//
|
||||
// Returns:
|
||||
// - auth.CredentialsListener: The listener to subscribe to the credentials provider
|
||||
// - error: Non-nil if poolCn is nil
|
||||
//
|
||||
// Note: The reAuth and onErr callbacks are captured once when the listener is
|
||||
// created and reused for the connection's lifetime. They should not change.
|
||||
//
|
||||
// Thread-safe: Can be called concurrently during connection initialization.
|
||||
func (m *Manager) Listener(
|
||||
poolCn *pool.Conn,
|
||||
reAuth func(*pool.Conn, auth.Credentials) error,
|
||||
onErr func(*pool.Conn, error),
|
||||
) (auth.CredentialsListener, error) {
|
||||
if poolCn == nil {
|
||||
return nil, errors.New("poolCn cannot be nil")
|
||||
}
|
||||
connID := poolCn.GetID()
|
||||
// if we reconnect the underlying network connection, the streaming credentials listener will continue to work
|
||||
// so we can get the old listener from the cache and use it.
|
||||
// subscribing the same (an already subscribed) listener for a StreamingCredentialsProvider SHOULD be a no-op
|
||||
listener, ok := m.credentialsListeners.Get(connID)
|
||||
if !ok || listener == nil {
|
||||
// Create new listener for this connection
|
||||
// Note: Callbacks (reAuth, onErr) are captured once and reused for the connection's lifetime
|
||||
newCredListener := &ConnReAuthCredentialsListener{
|
||||
conn: poolCn,
|
||||
reAuth: reAuth,
|
||||
onErr: onErr,
|
||||
manager: m,
|
||||
}
|
||||
|
||||
m.credentialsListeners.Add(connID, newCredListener)
|
||||
listener = newCredListener
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
// MarkForReAuth marks a connection for re-authentication.
|
||||
//
|
||||
// This method is called by the credentials listener when new credentials are
|
||||
// received. It delegates to the pool hook to schedule background re-authentication.
|
||||
//
|
||||
// Parameters:
|
||||
// - poolCn: The connection to re-authenticate
|
||||
// - reAuthFn: Function to call for re-authentication, receives error if acquisition fails
|
||||
//
|
||||
// Thread-safe: Called by credentials listeners when credentials change.
|
||||
func (m *Manager) MarkForReAuth(poolCn *pool.Conn, reAuthFn func(error)) {
|
||||
connID := poolCn.GetID()
|
||||
m.poolHookRef.MarkForReAuth(connID, reAuthFn)
|
||||
}
|
||||
|
||||
// RemoveListener removes the credentials listener for a connection.
|
||||
//
|
||||
// This method is called by the pool hook's OnRemove to clean up listeners
|
||||
// when connections are removed from the pool.
|
||||
//
|
||||
// Parameters:
|
||||
// - connID: The connection ID whose listener should be removed
|
||||
//
|
||||
// Thread-safe: Called during connection removal.
|
||||
func (m *Manager) RemoveListener(connID uint64) {
|
||||
m.credentialsListeners.Remove(connID)
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
)
|
||||
|
||||
// ReAuthPoolHook is a pool hook that manages background re-authentication of connections
|
||||
// when credentials change via a streaming credentials provider.
|
||||
//
|
||||
// The hook uses a semaphore-based worker pool to limit concurrent re-authentication
|
||||
// operations and prevent pool exhaustion. When credentials change, connections are
|
||||
// marked for re-authentication and processed asynchronously in the background.
|
||||
//
|
||||
// The re-authentication process:
|
||||
// 1. OnPut: When a connection is returned to the pool, check if it needs re-auth
|
||||
// 2. If yes, schedule it for background processing (move from shouldReAuth to scheduledReAuth)
|
||||
// 3. A worker goroutine acquires the connection (waits until it's not in use)
|
||||
// 4. Executes the re-auth function while holding the connection
|
||||
// 5. Releases the connection back to the pool
|
||||
//
|
||||
// The hook ensures that:
|
||||
// - Only one re-auth operation runs per connection at a time
|
||||
// - Connections are not used for commands during re-authentication
|
||||
// - Re-auth operations timeout if they can't acquire the connection
|
||||
// - Resources are properly cleaned up on connection removal
|
||||
type ReAuthPoolHook struct {
|
||||
// shouldReAuth maps connection ID to re-auth function
|
||||
// Connections in this map need re-authentication but haven't been scheduled yet
|
||||
shouldReAuth map[uint64]func(error)
|
||||
shouldReAuthLock sync.RWMutex
|
||||
|
||||
// workers is a semaphore limiting concurrent re-auth operations
|
||||
// Initialized with poolSize tokens to prevent pool exhaustion
|
||||
// Uses FastSemaphore for better performance with eventual fairness
|
||||
workers *internal.FastSemaphore
|
||||
|
||||
// reAuthTimeout is the maximum time to wait for acquiring a connection for re-auth
|
||||
reAuthTimeout time.Duration
|
||||
|
||||
// scheduledReAuth maps connection ID to scheduled status
|
||||
// Connections in this map have a background worker attempting re-authentication
|
||||
scheduledReAuth map[uint64]bool
|
||||
scheduledLock sync.RWMutex
|
||||
|
||||
// manager is a back-reference for cleanup operations
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
// NewReAuthPoolHook creates a new re-authentication pool hook.
|
||||
//
|
||||
// Parameters:
|
||||
// - poolSize: Maximum number of concurrent re-auth operations (typically matches pool size)
|
||||
// - reAuthTimeout: Maximum time to wait for acquiring a connection for re-authentication
|
||||
//
|
||||
// The poolSize parameter is used to initialize the worker semaphore, ensuring that
|
||||
// re-auth operations don't exhaust the connection pool.
|
||||
func NewReAuthPoolHook(poolSize int, reAuthTimeout time.Duration) *ReAuthPoolHook {
|
||||
return &ReAuthPoolHook{
|
||||
shouldReAuth: make(map[uint64]func(error)),
|
||||
scheduledReAuth: make(map[uint64]bool),
|
||||
workers: internal.NewFastSemaphore(int32(poolSize)),
|
||||
reAuthTimeout: reAuthTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// MarkForReAuth marks a connection for re-authentication.
|
||||
//
|
||||
// This method is called when credentials change and a connection needs to be
|
||||
// re-authenticated. The actual re-authentication happens asynchronously when
|
||||
// the connection is returned to the pool (in OnPut).
|
||||
//
|
||||
// Parameters:
|
||||
// - connID: The connection ID to mark for re-authentication
|
||||
// - reAuthFn: Function to call for re-authentication, receives error if acquisition fails
|
||||
//
|
||||
// Thread-safe: Can be called concurrently from multiple goroutines.
|
||||
func (r *ReAuthPoolHook) MarkForReAuth(connID uint64, reAuthFn func(error)) {
|
||||
r.shouldReAuthLock.Lock()
|
||||
defer r.shouldReAuthLock.Unlock()
|
||||
r.shouldReAuth[connID] = reAuthFn
|
||||
}
|
||||
|
||||
// OnGet is called when a connection is retrieved from the pool.
|
||||
//
|
||||
// This hook checks if the connection needs re-authentication or has a scheduled
|
||||
// re-auth operation. If so, it rejects the connection (returns accept=false),
|
||||
// causing the pool to try another connection.
|
||||
//
|
||||
// Returns:
|
||||
// - accept: false if connection needs re-auth, true otherwise
|
||||
// - err: always nil (errors are not used in this hook)
|
||||
//
|
||||
// Thread-safe: Called concurrently by multiple goroutines getting connections.
|
||||
func (r *ReAuthPoolHook) OnGet(_ context.Context, conn *pool.Conn, _ bool) (accept bool, err error) {
|
||||
connID := conn.GetID()
|
||||
r.shouldReAuthLock.RLock()
|
||||
_, shouldReAuth := r.shouldReAuth[connID]
|
||||
r.shouldReAuthLock.RUnlock()
|
||||
// This connection was marked for reauth while in the pool,
|
||||
// reject the connection
|
||||
if shouldReAuth {
|
||||
// simply reject the connection, it will be re-authenticated in OnPut
|
||||
return false, nil
|
||||
}
|
||||
r.scheduledLock.RLock()
|
||||
_, hasScheduled := r.scheduledReAuth[connID]
|
||||
r.scheduledLock.RUnlock()
|
||||
// has scheduled reauth, reject the connection
|
||||
if hasScheduled {
|
||||
// simply reject the connection, it currently has a reauth scheduled
|
||||
// and the worker is waiting for slot to execute the reauth
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// OnPut is called when a connection is returned to the pool.
|
||||
//
|
||||
// This hook checks if the connection needs re-authentication. If so, it schedules
|
||||
// a background goroutine to perform the re-auth asynchronously. The goroutine:
|
||||
// 1. Waits for a worker slot (semaphore)
|
||||
// 2. Acquires the connection (waits until not in use)
|
||||
// 3. Executes the re-auth function
|
||||
// 4. Releases the connection and worker slot
|
||||
//
|
||||
// The connection is always pooled (not removed) since re-auth happens in background.
|
||||
//
|
||||
// Returns:
|
||||
// - shouldPool: always true (connection stays in pool during background re-auth)
|
||||
// - shouldRemove: always false
|
||||
// - err: always nil
|
||||
//
|
||||
// Thread-safe: Called concurrently by multiple goroutines returning connections.
|
||||
func (r *ReAuthPoolHook) OnPut(_ context.Context, conn *pool.Conn) (bool, bool, error) {
|
||||
if conn == nil {
|
||||
// noop
|
||||
return true, false, nil
|
||||
}
|
||||
connID := conn.GetID()
|
||||
// Check if reauth is needed and get the function with proper locking
|
||||
r.shouldReAuthLock.RLock()
|
||||
reAuthFn, ok := r.shouldReAuth[connID]
|
||||
r.shouldReAuthLock.RUnlock()
|
||||
|
||||
if ok {
|
||||
// Acquire both locks to atomically move from shouldReAuth to scheduledReAuth
|
||||
// This prevents race conditions where OnGet might miss the transition
|
||||
r.shouldReAuthLock.Lock()
|
||||
r.scheduledLock.Lock()
|
||||
r.scheduledReAuth[connID] = true
|
||||
delete(r.shouldReAuth, connID)
|
||||
r.scheduledLock.Unlock()
|
||||
r.shouldReAuthLock.Unlock()
|
||||
go func() {
|
||||
r.workers.AcquireBlocking()
|
||||
// safety first
|
||||
if conn == nil || (conn != nil && conn.IsClosed()) {
|
||||
r.workers.Release()
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
// once again - safety first
|
||||
internal.Logger.Printf(context.Background(), "panic in reauth worker: %v", rec)
|
||||
}
|
||||
r.scheduledLock.Lock()
|
||||
delete(r.scheduledReAuth, connID)
|
||||
r.scheduledLock.Unlock()
|
||||
r.workers.Release()
|
||||
}()
|
||||
|
||||
// Create timeout context for connection acquisition
|
||||
// This prevents indefinite waiting if the connection is stuck
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.reAuthTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Try to acquire the connection for re-authentication
|
||||
// We need to ensure the connection is IDLE (not IN_USE) before transitioning to UNUSABLE
|
||||
// This prevents re-authentication from interfering with active commands
|
||||
// Use AwaitAndTransition to wait for the connection to become IDLE
|
||||
stateMachine := conn.GetStateMachine()
|
||||
if stateMachine == nil {
|
||||
// No state machine - should not happen, but handle gracefully
|
||||
reAuthFn(pool.ErrConnUnusableTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
// Use predefined slice to avoid allocation
|
||||
_, err := stateMachine.AwaitAndTransition(ctx, pool.ValidFromIdle(), pool.StateUnusable)
|
||||
if err != nil {
|
||||
// Timeout or other error occurred, cannot acquire connection
|
||||
reAuthFn(err)
|
||||
return
|
||||
}
|
||||
|
||||
// safety first
|
||||
if !conn.IsClosed() {
|
||||
// Successfully acquired the connection, perform reauth
|
||||
reAuthFn(nil)
|
||||
}
|
||||
|
||||
// Release the connection: transition from UNUSABLE back to IDLE
|
||||
stateMachine.Transition(pool.StateIdle)
|
||||
}()
|
||||
}
|
||||
|
||||
// the reauth will happen in background, as far as the pool is concerned:
|
||||
// pool the connection, don't remove it, no error
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
// OnRemove is called when a connection is removed from the pool.
|
||||
//
|
||||
// This hook cleans up all state associated with the connection:
|
||||
// - Removes from shouldReAuth map (pending re-auth)
|
||||
// - Removes from scheduledReAuth map (active re-auth)
|
||||
// - Removes credentials listener from manager
|
||||
//
|
||||
// This prevents memory leaks and ensures that removed connections don't have
|
||||
// lingering re-auth operations or listeners.
|
||||
//
|
||||
// Thread-safe: Called when connections are removed due to errors, timeouts, or pool closure.
|
||||
func (r *ReAuthPoolHook) OnRemove(_ context.Context, conn *pool.Conn, _ error) {
|
||||
connID := conn.GetID()
|
||||
r.shouldReAuthLock.Lock()
|
||||
r.scheduledLock.Lock()
|
||||
delete(r.scheduledReAuth, connID)
|
||||
delete(r.shouldReAuth, connID)
|
||||
r.scheduledLock.Unlock()
|
||||
r.shouldReAuthLock.Unlock()
|
||||
if r.manager != nil {
|
||||
r.manager.RemoveListener(connID)
|
||||
}
|
||||
}
|
||||
|
||||
var _ pool.PoolHook = (*ReAuthPoolHook)(nil)
|
||||
+12
@@ -56,6 +56,18 @@ func Key(key string) string {
|
||||
return key
|
||||
}
|
||||
|
||||
func Present(key string) bool {
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
if s := strings.IndexByte(key, '{'); s > -1 {
|
||||
if e := strings.IndexByte(key[s+1:], '}'); e > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RandomSlot() int {
|
||||
return rand.Intn(slotNumber)
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Package interfaces provides shared interfaces used by both the main redis package
|
||||
// and the maintnotifications upgrade package to avoid circular dependencies.
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NotificationProcessor is (most probably) a push.NotificationProcessor
|
||||
// forward declaration to avoid circular imports
|
||||
type NotificationProcessor interface {
|
||||
RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error
|
||||
UnregisterHandler(pushNotificationName string) error
|
||||
GetHandler(pushNotificationName string) interface{}
|
||||
}
|
||||
|
||||
// ClientInterface defines the interface that clients must implement for maintnotifications upgrades.
|
||||
type ClientInterface interface {
|
||||
// GetOptions returns the client options.
|
||||
GetOptions() OptionsInterface
|
||||
|
||||
// GetPushProcessor returns the client's push notification processor.
|
||||
GetPushProcessor() NotificationProcessor
|
||||
}
|
||||
|
||||
// OptionsInterface defines the interface for client options.
|
||||
// Uses an adapter pattern to avoid circular dependencies.
|
||||
type OptionsInterface interface {
|
||||
// GetReadTimeout returns the read timeout.
|
||||
GetReadTimeout() time.Duration
|
||||
|
||||
// GetWriteTimeout returns the write timeout.
|
||||
GetWriteTimeout() time.Duration
|
||||
|
||||
// GetNetwork returns the network type.
|
||||
GetNetwork() string
|
||||
|
||||
// GetAddr returns the connection address.
|
||||
GetAddr() string
|
||||
|
||||
// IsTLSEnabled returns true if TLS is enabled.
|
||||
IsTLSEnabled() bool
|
||||
|
||||
// GetProtocol returns the protocol version.
|
||||
GetProtocol() int
|
||||
|
||||
// GetPoolSize returns the connection pool size.
|
||||
GetPoolSize() int
|
||||
|
||||
// NewDialer returns a new dialer function for the connection.
|
||||
NewDialer() func(context.Context) (net.Conn, error)
|
||||
}
|
||||
+57
-4
@@ -7,20 +7,73 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// TODO (ned): Revisit logging
|
||||
// Add more standardized approach with log levels and configurability
|
||||
|
||||
type Logging interface {
|
||||
Printf(ctx context.Context, format string, v ...interface{})
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
type DefaultLogger struct {
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
func (l *logger) Printf(ctx context.Context, format string, v ...interface{}) {
|
||||
func (l *DefaultLogger) Printf(ctx context.Context, format string, v ...interface{}) {
|
||||
_ = l.log.Output(2, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func NewDefaultLogger() Logging {
|
||||
return &DefaultLogger{
|
||||
log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile),
|
||||
}
|
||||
}
|
||||
|
||||
// Logger calls Output to print to the stderr.
|
||||
// Arguments are handled in the manner of fmt.Print.
|
||||
var Logger Logging = &logger{
|
||||
log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile),
|
||||
var Logger Logging = NewDefaultLogger()
|
||||
|
||||
var LogLevel LogLevelT = LogLevelError
|
||||
|
||||
// LogLevelT represents the logging level
|
||||
type LogLevelT int
|
||||
|
||||
// Log level constants for the entire go-redis library
|
||||
const (
|
||||
LogLevelError LogLevelT = iota // 0 - errors only
|
||||
LogLevelWarn // 1 - warnings and errors
|
||||
LogLevelInfo // 2 - info, warnings, and errors
|
||||
LogLevelDebug // 3 - debug, info, warnings, and errors
|
||||
)
|
||||
|
||||
// String returns the string representation of the log level
|
||||
func (l LogLevelT) String() string {
|
||||
switch l {
|
||||
case LogLevelError:
|
||||
return "ERROR"
|
||||
case LogLevelWarn:
|
||||
return "WARN"
|
||||
case LogLevelInfo:
|
||||
return "INFO"
|
||||
case LogLevelDebug:
|
||||
return "DEBUG"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns true if the log level is valid
|
||||
func (l LogLevelT) IsValid() bool {
|
||||
return l >= LogLevelError && l <= LogLevelDebug
|
||||
}
|
||||
|
||||
func (l LogLevelT) WarnOrAbove() bool {
|
||||
return l >= LogLevelWarn
|
||||
}
|
||||
|
||||
func (l LogLevelT) InfoOrAbove() bool {
|
||||
return l >= LogLevelInfo
|
||||
}
|
||||
|
||||
func (l LogLevelT) DebugOrAbove() bool {
|
||||
return l >= LogLevelDebug
|
||||
}
|
||||
|
||||
Generated
Vendored
+625
@@ -0,0 +1,625 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
)
|
||||
|
||||
// appendJSONIfDebug appends JSON data to a message only if the global log level is Debug
|
||||
func appendJSONIfDebug(message string, data map[string]interface{}) string {
|
||||
if internal.LogLevel.DebugOrAbove() {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return fmt.Sprintf("%s %s", message, string(jsonData))
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
const (
|
||||
// ========================================
|
||||
// CIRCUIT_BREAKER.GO - Circuit breaker management
|
||||
// ========================================
|
||||
CircuitBreakerTransitioningToHalfOpenMessage = "circuit breaker transitioning to half-open"
|
||||
CircuitBreakerOpenedMessage = "circuit breaker opened"
|
||||
CircuitBreakerReopenedMessage = "circuit breaker reopened"
|
||||
CircuitBreakerClosedMessage = "circuit breaker closed"
|
||||
CircuitBreakerCleanupMessage = "circuit breaker cleanup"
|
||||
CircuitBreakerOpenMessage = "circuit breaker is open, failing fast"
|
||||
|
||||
// ========================================
|
||||
// CONFIG.GO - Configuration and debug
|
||||
// ========================================
|
||||
DebugLoggingEnabledMessage = "debug logging enabled"
|
||||
ConfigDebugMessage = "config debug"
|
||||
|
||||
// ========================================
|
||||
// ERRORS.GO - Error message constants
|
||||
// ========================================
|
||||
InvalidRelaxedTimeoutErrorMessage = "relaxed timeout must be greater than 0"
|
||||
InvalidHandoffTimeoutErrorMessage = "handoff timeout must be greater than 0"
|
||||
InvalidHandoffWorkersErrorMessage = "MaxWorkers must be greater than or equal to 0"
|
||||
InvalidHandoffQueueSizeErrorMessage = "handoff queue size must be greater than 0"
|
||||
InvalidPostHandoffRelaxedDurationErrorMessage = "post-handoff relaxed duration must be greater than or equal to 0"
|
||||
InvalidEndpointTypeErrorMessage = "invalid endpoint type"
|
||||
InvalidMaintNotificationsErrorMessage = "invalid maintenance notifications setting (must be 'disabled', 'enabled', or 'auto')"
|
||||
InvalidHandoffRetriesErrorMessage = "MaxHandoffRetries must be between 1 and 10"
|
||||
InvalidClientErrorMessage = "invalid client type"
|
||||
InvalidNotificationErrorMessage = "invalid notification format"
|
||||
MaxHandoffRetriesReachedErrorMessage = "max handoff retries reached"
|
||||
HandoffQueueFullErrorMessage = "handoff queue is full, cannot queue new handoff requests - consider increasing HandoffQueueSize or MaxWorkers in configuration"
|
||||
InvalidCircuitBreakerFailureThresholdErrorMessage = "circuit breaker failure threshold must be >= 1"
|
||||
InvalidCircuitBreakerResetTimeoutErrorMessage = "circuit breaker reset timeout must be >= 0"
|
||||
InvalidCircuitBreakerMaxRequestsErrorMessage = "circuit breaker max requests must be >= 1"
|
||||
ConnectionMarkedForHandoffErrorMessage = "connection marked for handoff"
|
||||
ConnectionInvalidHandoffStateErrorMessage = "connection is in invalid state for handoff"
|
||||
ShutdownErrorMessage = "shutdown"
|
||||
CircuitBreakerOpenErrorMessage = "circuit breaker is open, failing fast"
|
||||
|
||||
// ========================================
|
||||
// EXAMPLE_HOOKS.GO - Example metrics hooks
|
||||
// ========================================
|
||||
MetricsHookProcessingNotificationMessage = "metrics hook processing"
|
||||
MetricsHookRecordedErrorMessage = "metrics hook recorded error"
|
||||
|
||||
// ========================================
|
||||
// HANDOFF_WORKER.GO - Connection handoff processing
|
||||
// ========================================
|
||||
HandoffStartedMessage = "handoff started"
|
||||
HandoffFailedMessage = "handoff failed"
|
||||
ConnectionNotMarkedForHandoffMessage = "is not marked for handoff and has no retries"
|
||||
ConnectionNotMarkedForHandoffErrorMessage = "is not marked for handoff"
|
||||
HandoffRetryAttemptMessage = "Performing handoff"
|
||||
CannotQueueHandoffForRetryMessage = "can't queue handoff for retry"
|
||||
HandoffQueueFullMessage = "handoff queue is full"
|
||||
FailedToDialNewEndpointMessage = "failed to dial new endpoint"
|
||||
ApplyingRelaxedTimeoutDueToPostHandoffMessage = "applying relaxed timeout due to post-handoff"
|
||||
HandoffSuccessMessage = "handoff succeeded"
|
||||
RemovingConnectionFromPoolMessage = "removing connection from pool"
|
||||
NoPoolProvidedMessageCannotRemoveMessage = "no pool provided, cannot remove connection, closing it"
|
||||
WorkerExitingDueToShutdownMessage = "worker exiting due to shutdown"
|
||||
WorkerExitingDueToShutdownWhileProcessingMessage = "worker exiting due to shutdown while processing request"
|
||||
WorkerPanicRecoveredMessage = "worker panic recovered"
|
||||
WorkerExitingDueToInactivityTimeoutMessage = "worker exiting due to inactivity timeout"
|
||||
ReachedMaxHandoffRetriesMessage = "reached max handoff retries"
|
||||
|
||||
// ========================================
|
||||
// MANAGER.GO - Moving operation tracking and handler registration
|
||||
// ========================================
|
||||
DuplicateMovingOperationMessage = "duplicate MOVING operation ignored"
|
||||
TrackingMovingOperationMessage = "tracking MOVING operation"
|
||||
UntrackingMovingOperationMessage = "untracking MOVING operation"
|
||||
OperationNotTrackedMessage = "operation not tracked"
|
||||
FailedToRegisterHandlerMessage = "failed to register handler"
|
||||
|
||||
// ========================================
|
||||
// HOOKS.GO - Notification processing hooks
|
||||
// ========================================
|
||||
ProcessingNotificationMessage = "processing notification started"
|
||||
ProcessingNotificationFailedMessage = "proccessing notification failed"
|
||||
ProcessingNotificationSucceededMessage = "processing notification succeeded"
|
||||
|
||||
// ========================================
|
||||
// POOL_HOOK.GO - Pool connection management
|
||||
// ========================================
|
||||
FailedToQueueHandoffMessage = "failed to queue handoff"
|
||||
MarkedForHandoffMessage = "connection marked for handoff"
|
||||
|
||||
// ========================================
|
||||
// PUSH_NOTIFICATION_HANDLER.GO - Push notification validation and processing
|
||||
// ========================================
|
||||
InvalidNotificationFormatMessage = "invalid notification format"
|
||||
InvalidNotificationTypeFormatMessage = "invalid notification type format"
|
||||
InvalidSeqIDInMovingNotificationMessage = "invalid seqID in MOVING notification"
|
||||
InvalidTimeSInMovingNotificationMessage = "invalid timeS in MOVING notification"
|
||||
InvalidNewEndpointInMovingNotificationMessage = "invalid newEndpoint in MOVING notification"
|
||||
NoConnectionInHandlerContextMessage = "no connection in handler context"
|
||||
InvalidConnectionTypeInHandlerContextMessage = "invalid connection type in handler context"
|
||||
SchedulingHandoffToCurrentEndpointMessage = "scheduling handoff to current endpoint"
|
||||
RelaxedTimeoutDueToNotificationMessage = "applying relaxed timeout due to notification"
|
||||
UnrelaxedTimeoutMessage = "clearing relaxed timeout"
|
||||
ManagerNotInitializedMessage = "manager not initialized"
|
||||
FailedToMarkForHandoffMessage = "failed to mark connection for handoff"
|
||||
|
||||
// ========================================
|
||||
// used in pool/conn
|
||||
// ========================================
|
||||
UnrelaxedTimeoutAfterDeadlineMessage = "clearing relaxed timeout after deadline"
|
||||
)
|
||||
|
||||
func HandoffStarted(connID uint64, newEndpoint string) string {
|
||||
message := fmt.Sprintf("conn[%d] %s to %s", connID, HandoffStartedMessage, newEndpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": newEndpoint,
|
||||
})
|
||||
}
|
||||
|
||||
func HandoffFailed(connID uint64, newEndpoint string, attempt int, maxAttempts int, err error) string {
|
||||
message := fmt.Sprintf("conn[%d] %s to %s (attempt %d/%d): %v", connID, HandoffFailedMessage, newEndpoint, attempt, maxAttempts, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": newEndpoint,
|
||||
"attempt": attempt,
|
||||
"maxAttempts": maxAttempts,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func HandoffSucceeded(connID uint64, newEndpoint string) string {
|
||||
message := fmt.Sprintf("conn[%d] %s to %s", connID, HandoffSuccessMessage, newEndpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": newEndpoint,
|
||||
})
|
||||
}
|
||||
|
||||
// Timeout-related log functions
|
||||
func RelaxedTimeoutDueToNotification(connID uint64, notificationType string, timeout interface{}) string {
|
||||
message := fmt.Sprintf("conn[%d] %s %s (%v)", connID, RelaxedTimeoutDueToNotificationMessage, notificationType, timeout)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"notificationType": notificationType,
|
||||
"timeout": fmt.Sprintf("%v", timeout),
|
||||
})
|
||||
}
|
||||
|
||||
func UnrelaxedTimeout(connID uint64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s", connID, UnrelaxedTimeoutMessage)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
})
|
||||
}
|
||||
|
||||
func UnrelaxedTimeoutAfterDeadline(connID uint64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s", connID, UnrelaxedTimeoutAfterDeadlineMessage)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
})
|
||||
}
|
||||
|
||||
// Handoff queue and marking functions
|
||||
func HandoffQueueFull(queueLen, queueCap int) string {
|
||||
message := fmt.Sprintf("%s (%d/%d), cannot queue new handoff requests - consider increasing HandoffQueueSize or MaxWorkers in configuration", HandoffQueueFullMessage, queueLen, queueCap)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"queueLen": queueLen,
|
||||
"queueCap": queueCap,
|
||||
})
|
||||
}
|
||||
|
||||
func FailedToQueueHandoff(connID uint64, err error) string {
|
||||
message := fmt.Sprintf("conn[%d] %s: %v", connID, FailedToQueueHandoffMessage, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func FailedToMarkForHandoff(connID uint64, err error) string {
|
||||
message := fmt.Sprintf("conn[%d] %s: %v", connID, FailedToMarkForHandoffMessage, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func FailedToDialNewEndpoint(connID uint64, endpoint string, err error) string {
|
||||
message := fmt.Sprintf("conn[%d] %s %s: %v", connID, FailedToDialNewEndpointMessage, endpoint, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": endpoint,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func ReachedMaxHandoffRetries(connID uint64, endpoint string, maxRetries int) string {
|
||||
message := fmt.Sprintf("conn[%d] %s to %s (max retries: %d)", connID, ReachedMaxHandoffRetriesMessage, endpoint, maxRetries)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": endpoint,
|
||||
"maxRetries": maxRetries,
|
||||
})
|
||||
}
|
||||
|
||||
// Notification processing functions
|
||||
func ProcessingNotification(connID uint64, seqID int64, notificationType string, notification interface{}) string {
|
||||
message := fmt.Sprintf("conn[%d] seqID[%d] %s %s: %v", connID, seqID, ProcessingNotificationMessage, notificationType, notification)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"seqID": seqID,
|
||||
"notificationType": notificationType,
|
||||
"notification": fmt.Sprintf("%v", notification),
|
||||
})
|
||||
}
|
||||
|
||||
func ProcessingNotificationFailed(connID uint64, notificationType string, err error, notification interface{}) string {
|
||||
message := fmt.Sprintf("conn[%d] %s %s: %v - %v", connID, ProcessingNotificationFailedMessage, notificationType, err, notification)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"notificationType": notificationType,
|
||||
"error": err.Error(),
|
||||
"notification": fmt.Sprintf("%v", notification),
|
||||
})
|
||||
}
|
||||
|
||||
func ProcessingNotificationSucceeded(connID uint64, notificationType string) string {
|
||||
message := fmt.Sprintf("conn[%d] %s %s", connID, ProcessingNotificationSucceededMessage, notificationType)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"notificationType": notificationType,
|
||||
})
|
||||
}
|
||||
|
||||
// Moving operation tracking functions
|
||||
func DuplicateMovingOperation(connID uint64, endpoint string, seqID int64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s for %s seqID[%d]", connID, DuplicateMovingOperationMessage, endpoint, seqID)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": endpoint,
|
||||
"seqID": seqID,
|
||||
})
|
||||
}
|
||||
|
||||
func TrackingMovingOperation(connID uint64, endpoint string, seqID int64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s for %s seqID[%d]", connID, TrackingMovingOperationMessage, endpoint, seqID)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": endpoint,
|
||||
"seqID": seqID,
|
||||
})
|
||||
}
|
||||
|
||||
func UntrackingMovingOperation(connID uint64, seqID int64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s seqID[%d]", connID, UntrackingMovingOperationMessage, seqID)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"seqID": seqID,
|
||||
})
|
||||
}
|
||||
|
||||
func OperationNotTracked(connID uint64, seqID int64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s seqID[%d]", connID, OperationNotTrackedMessage, seqID)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"seqID": seqID,
|
||||
})
|
||||
}
|
||||
|
||||
// Connection pool functions
|
||||
func RemovingConnectionFromPool(connID uint64, reason error) string {
|
||||
message := fmt.Sprintf("conn[%d] %s due to: %v", connID, RemovingConnectionFromPoolMessage, reason)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"reason": reason.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func NoPoolProvidedCannotRemove(connID uint64, reason error) string {
|
||||
message := fmt.Sprintf("conn[%d] %s due to: %v", connID, NoPoolProvidedMessageCannotRemoveMessage, reason)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"reason": reason.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Circuit breaker functions
|
||||
func CircuitBreakerOpen(connID uint64, endpoint string) string {
|
||||
message := fmt.Sprintf("conn[%d] %s for %s", connID, CircuitBreakerOpenMessage, endpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
// Additional handoff functions for specific cases
|
||||
func ConnectionNotMarkedForHandoff(connID uint64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s", connID, ConnectionNotMarkedForHandoffMessage)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
})
|
||||
}
|
||||
|
||||
func ConnectionNotMarkedForHandoffError(connID uint64) string {
|
||||
return fmt.Sprintf("conn[%d] %s", connID, ConnectionNotMarkedForHandoffErrorMessage)
|
||||
}
|
||||
|
||||
func HandoffRetryAttempt(connID uint64, retries int, newEndpoint string, oldEndpoint string) string {
|
||||
message := fmt.Sprintf("conn[%d] Retry %d: %s to %s(was %s)", connID, retries, HandoffRetryAttemptMessage, newEndpoint, oldEndpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"retries": retries,
|
||||
"newEndpoint": newEndpoint,
|
||||
"oldEndpoint": oldEndpoint,
|
||||
})
|
||||
}
|
||||
|
||||
func CannotQueueHandoffForRetry(err error) string {
|
||||
message := fmt.Sprintf("%s: %v", CannotQueueHandoffForRetryMessage, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Validation and error functions
|
||||
func InvalidNotificationFormat(notification interface{}) string {
|
||||
message := fmt.Sprintf("%s: %v", InvalidNotificationFormatMessage, notification)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notification": fmt.Sprintf("%v", notification),
|
||||
})
|
||||
}
|
||||
|
||||
func InvalidNotificationTypeFormat(notificationType interface{}) string {
|
||||
message := fmt.Sprintf("%s: %v", InvalidNotificationTypeFormatMessage, notificationType)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": fmt.Sprintf("%v", notificationType),
|
||||
})
|
||||
}
|
||||
|
||||
// InvalidNotification creates a log message for invalid notifications of any type
|
||||
func InvalidNotification(notificationType string, notification interface{}) string {
|
||||
message := fmt.Sprintf("invalid %s notification: %v", notificationType, notification)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": notificationType,
|
||||
"notification": fmt.Sprintf("%v", notification),
|
||||
})
|
||||
}
|
||||
|
||||
func InvalidSeqIDInMovingNotification(seqID interface{}) string {
|
||||
message := fmt.Sprintf("%s: %v", InvalidSeqIDInMovingNotificationMessage, seqID)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"seqID": fmt.Sprintf("%v", seqID),
|
||||
})
|
||||
}
|
||||
|
||||
func InvalidTimeSInMovingNotification(timeS interface{}) string {
|
||||
message := fmt.Sprintf("%s: %v", InvalidTimeSInMovingNotificationMessage, timeS)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"timeS": fmt.Sprintf("%v", timeS),
|
||||
})
|
||||
}
|
||||
|
||||
func InvalidNewEndpointInMovingNotification(newEndpoint interface{}) string {
|
||||
message := fmt.Sprintf("%s: %v", InvalidNewEndpointInMovingNotificationMessage, newEndpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"newEndpoint": fmt.Sprintf("%v", newEndpoint),
|
||||
})
|
||||
}
|
||||
|
||||
func NoConnectionInHandlerContext(notificationType string) string {
|
||||
message := fmt.Sprintf("%s for %s notification", NoConnectionInHandlerContextMessage, notificationType)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": notificationType,
|
||||
})
|
||||
}
|
||||
|
||||
func InvalidConnectionTypeInHandlerContext(notificationType string, conn interface{}, handlerCtx interface{}) string {
|
||||
message := fmt.Sprintf("%s for %s notification - %T %#v", InvalidConnectionTypeInHandlerContextMessage, notificationType, conn, handlerCtx)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": notificationType,
|
||||
"connType": fmt.Sprintf("%T", conn),
|
||||
})
|
||||
}
|
||||
|
||||
func SchedulingHandoffToCurrentEndpoint(connID uint64, seconds float64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s in %v seconds", connID, SchedulingHandoffToCurrentEndpointMessage, seconds)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"seconds": seconds,
|
||||
})
|
||||
}
|
||||
|
||||
func ManagerNotInitialized() string {
|
||||
return appendJSONIfDebug(ManagerNotInitializedMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func FailedToRegisterHandler(notificationType string, err error) string {
|
||||
message := fmt.Sprintf("%s for %s: %v", FailedToRegisterHandlerMessage, notificationType, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": notificationType,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func ShutdownError() string {
|
||||
return appendJSONIfDebug(ShutdownErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
// Configuration validation error functions
|
||||
func InvalidRelaxedTimeoutError() string {
|
||||
return appendJSONIfDebug(InvalidRelaxedTimeoutErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidHandoffTimeoutError() string {
|
||||
return appendJSONIfDebug(InvalidHandoffTimeoutErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidHandoffWorkersError() string {
|
||||
return appendJSONIfDebug(InvalidHandoffWorkersErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidHandoffQueueSizeError() string {
|
||||
return appendJSONIfDebug(InvalidHandoffQueueSizeErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidPostHandoffRelaxedDurationError() string {
|
||||
return appendJSONIfDebug(InvalidPostHandoffRelaxedDurationErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidEndpointTypeError() string {
|
||||
return appendJSONIfDebug(InvalidEndpointTypeErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidMaintNotificationsError() string {
|
||||
return appendJSONIfDebug(InvalidMaintNotificationsErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidHandoffRetriesError() string {
|
||||
return appendJSONIfDebug(InvalidHandoffRetriesErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidClientError() string {
|
||||
return appendJSONIfDebug(InvalidClientErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidNotificationError() string {
|
||||
return appendJSONIfDebug(InvalidNotificationErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func MaxHandoffRetriesReachedError() string {
|
||||
return appendJSONIfDebug(MaxHandoffRetriesReachedErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func HandoffQueueFullError() string {
|
||||
return appendJSONIfDebug(HandoffQueueFullErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidCircuitBreakerFailureThresholdError() string {
|
||||
return appendJSONIfDebug(InvalidCircuitBreakerFailureThresholdErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidCircuitBreakerResetTimeoutError() string {
|
||||
return appendJSONIfDebug(InvalidCircuitBreakerResetTimeoutErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func InvalidCircuitBreakerMaxRequestsError() string {
|
||||
return appendJSONIfDebug(InvalidCircuitBreakerMaxRequestsErrorMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
// Configuration and debug functions
|
||||
func DebugLoggingEnabled() string {
|
||||
return appendJSONIfDebug(DebugLoggingEnabledMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func ConfigDebug(config interface{}) string {
|
||||
message := fmt.Sprintf("%s: %+v", ConfigDebugMessage, config)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"config": fmt.Sprintf("%+v", config),
|
||||
})
|
||||
}
|
||||
|
||||
// Handoff worker functions
|
||||
func WorkerExitingDueToShutdown() string {
|
||||
return appendJSONIfDebug(WorkerExitingDueToShutdownMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func WorkerExitingDueToShutdownWhileProcessing() string {
|
||||
return appendJSONIfDebug(WorkerExitingDueToShutdownWhileProcessingMessage, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func WorkerPanicRecovered(panicValue interface{}) string {
|
||||
message := fmt.Sprintf("%s: %v", WorkerPanicRecoveredMessage, panicValue)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"panic": fmt.Sprintf("%v", panicValue),
|
||||
})
|
||||
}
|
||||
|
||||
func WorkerExitingDueToInactivityTimeout(timeout interface{}) string {
|
||||
message := fmt.Sprintf("%s (%v)", WorkerExitingDueToInactivityTimeoutMessage, timeout)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"timeout": fmt.Sprintf("%v", timeout),
|
||||
})
|
||||
}
|
||||
|
||||
func ApplyingRelaxedTimeoutDueToPostHandoff(connID uint64, timeout interface{}, until string) string {
|
||||
message := fmt.Sprintf("conn[%d] %s (%v) until %s", connID, ApplyingRelaxedTimeoutDueToPostHandoffMessage, timeout, until)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
"timeout": fmt.Sprintf("%v", timeout),
|
||||
"until": until,
|
||||
})
|
||||
}
|
||||
|
||||
// Example hooks functions
|
||||
func MetricsHookProcessingNotification(notificationType string, connID uint64) string {
|
||||
message := fmt.Sprintf("%s %s notification on conn[%d]", MetricsHookProcessingNotificationMessage, notificationType, connID)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": notificationType,
|
||||
"connID": connID,
|
||||
})
|
||||
}
|
||||
|
||||
func MetricsHookRecordedError(notificationType string, connID uint64, err error) string {
|
||||
message := fmt.Sprintf("%s for %s notification on conn[%d]: %v", MetricsHookRecordedErrorMessage, notificationType, connID, err)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"notificationType": notificationType,
|
||||
"connID": connID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Pool hook functions
|
||||
func MarkedForHandoff(connID uint64) string {
|
||||
message := fmt.Sprintf("conn[%d] %s", connID, MarkedForHandoffMessage)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"connID": connID,
|
||||
})
|
||||
}
|
||||
|
||||
// Circuit breaker additional functions
|
||||
func CircuitBreakerTransitioningToHalfOpen(endpoint string) string {
|
||||
message := fmt.Sprintf("%s for %s", CircuitBreakerTransitioningToHalfOpenMessage, endpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
func CircuitBreakerOpened(endpoint string, failures int64) string {
|
||||
message := fmt.Sprintf("%s for endpoint %s after %d failures", CircuitBreakerOpenedMessage, endpoint, failures)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"endpoint": endpoint,
|
||||
"failures": failures,
|
||||
})
|
||||
}
|
||||
|
||||
func CircuitBreakerReopened(endpoint string) string {
|
||||
message := fmt.Sprintf("%s for endpoint %s due to failure in half-open state", CircuitBreakerReopenedMessage, endpoint)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
func CircuitBreakerClosed(endpoint string, successes int64) string {
|
||||
message := fmt.Sprintf("%s for endpoint %s after %d successful requests", CircuitBreakerClosedMessage, endpoint, successes)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"endpoint": endpoint,
|
||||
"successes": successes,
|
||||
})
|
||||
}
|
||||
|
||||
func CircuitBreakerCleanup(removed int, total int) string {
|
||||
message := fmt.Sprintf("%s removed %d/%d entries", CircuitBreakerCleanupMessage, removed, total)
|
||||
return appendJSONIfDebug(message, map[string]interface{}{
|
||||
"removed": removed,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// ExtractDataFromLogMessage extracts structured data from maintnotifications log messages
|
||||
// Returns a map containing the parsed key-value pairs from the structured data section
|
||||
// Example: "conn[123] handoff started to localhost:6379 {"connID":123,"endpoint":"localhost:6379"}"
|
||||
// Returns: map[string]interface{}{"connID": 123, "endpoint": "localhost:6379"}
|
||||
func ExtractDataFromLogMessage(logMessage string) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Find the JSON data section at the end of the message
|
||||
re := regexp.MustCompile(`(\{.*\})$`)
|
||||
matches := re.FindStringSubmatch(logMessage)
|
||||
if len(matches) < 2 {
|
||||
return result
|
||||
}
|
||||
|
||||
jsonStr := matches[1]
|
||||
if jsonStr == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
// Parse the JSON directly
|
||||
var jsonResult map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &jsonResult); err == nil {
|
||||
return jsonResult
|
||||
}
|
||||
|
||||
// If JSON parsing fails, return empty map
|
||||
return result
|
||||
}
|
||||
+818
-24
@@ -1,64 +1,803 @@
|
||||
// Package pool implements the pool management
|
||||
package pool
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
var noDeadline = time.Time{}
|
||||
|
||||
// Preallocated errors for hot paths to avoid allocations
|
||||
var (
|
||||
errAlreadyMarkedForHandoff = errors.New("connection is already marked for handoff")
|
||||
errNotMarkedForHandoff = errors.New("connection was not marked for handoff")
|
||||
errHandoffStateChanged = errors.New("handoff state changed during marking")
|
||||
errConnectionNotAvailable = errors.New("redis: connection not available")
|
||||
errConnNotAvailableForWrite = errors.New("redis: connection not available for write operation")
|
||||
)
|
||||
|
||||
// getCachedTimeNs returns the current time in nanoseconds.
|
||||
// This function previously used a global cache updated by a background goroutine,
|
||||
// but that caused unnecessary CPU usage when the client was idle (ticker waking up
|
||||
// the scheduler every 50ms). We now use time.Now() directly, which is fast enough
|
||||
// on modern systems (vDSO on Linux) and only adds ~1-2% overhead in extreme
|
||||
// high-concurrency benchmarks while eliminating idle CPU usage.
|
||||
func getCachedTimeNs() int64 {
|
||||
return time.Now().UnixNano()
|
||||
}
|
||||
|
||||
// GetCachedTimeNs returns the current time in nanoseconds.
|
||||
// Exported for use by other packages that need fast time access.
|
||||
func GetCachedTimeNs() int64 {
|
||||
return getCachedTimeNs()
|
||||
}
|
||||
|
||||
// Global atomic counter for connection IDs
|
||||
var connIDCounter uint64
|
||||
|
||||
// HandoffState represents the atomic state for connection handoffs
|
||||
// This struct is stored atomically to prevent race conditions between
|
||||
// checking handoff status and reading handoff parameters
|
||||
type HandoffState struct {
|
||||
ShouldHandoff bool // Whether connection should be handed off
|
||||
Endpoint string // New endpoint for handoff
|
||||
SeqID int64 // Sequence ID from MOVING notification
|
||||
}
|
||||
|
||||
// atomicNetConn is a wrapper to ensure consistent typing in atomic.Value
|
||||
type atomicNetConn struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// generateConnID generates a fast unique identifier for a connection with zero allocations
|
||||
func generateConnID() uint64 {
|
||||
return atomic.AddUint64(&connIDCounter, 1)
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
usedAt int64 // atomic
|
||||
netConn net.Conn
|
||||
// Connection identifier for unique tracking
|
||||
id uint64
|
||||
|
||||
usedAt atomic.Int64
|
||||
lastPutAt atomic.Int64
|
||||
|
||||
// Lock-free netConn access using atomic.Value
|
||||
// Contains *atomicNetConn wrapper, accessed atomically for better performance
|
||||
netConnAtomic atomic.Value // stores *atomicNetConn
|
||||
|
||||
rd *proto.Reader
|
||||
bw *bufio.Writer
|
||||
wr *proto.Writer
|
||||
|
||||
Inited bool
|
||||
// Lightweight mutex to protect reader operations during handoff
|
||||
// Only used for the brief period during SetNetConn and HasBufferedData/PeekReplyTypeSafe
|
||||
readerMu sync.RWMutex
|
||||
|
||||
// State machine for connection state management
|
||||
// Replaces: usable, Inited, used
|
||||
// Provides thread-safe state transitions with FIFO waiting queue
|
||||
// States: CREATED → INITIALIZING → IDLE ⇄ IN_USE
|
||||
// ↓
|
||||
// UNUSABLE (handoff/reauth)
|
||||
// ↓
|
||||
// IDLE/CLOSED
|
||||
stateMachine *ConnStateMachine
|
||||
|
||||
// Handoff metadata - managed separately from state machine
|
||||
// These are atomic for lock-free access during handoff operations
|
||||
handoffStateAtomic atomic.Value // stores *HandoffState
|
||||
handoffRetriesAtomic atomic.Uint32 // retry counter
|
||||
|
||||
pooled bool
|
||||
pubsub bool
|
||||
closed atomic.Bool
|
||||
createdAt time.Time
|
||||
expiresAt time.Time
|
||||
|
||||
// maintenanceNotifications upgrade support: relaxed timeouts during migrations/failovers
|
||||
|
||||
// Using atomic operations for lock-free access to avoid mutex contention
|
||||
relaxedReadTimeoutNs atomic.Int64 // time.Duration as nanoseconds
|
||||
relaxedWriteTimeoutNs atomic.Int64 // time.Duration as nanoseconds
|
||||
relaxedDeadlineNs atomic.Int64 // time.Time as nanoseconds since epoch
|
||||
|
||||
// Counter to track multiple relaxed timeout setters if we have nested calls
|
||||
// will be decremented when ClearRelaxedTimeout is called or deadline is reached
|
||||
// if counter reaches 0, we clear the relaxed timeouts
|
||||
relaxedCounter atomic.Int32
|
||||
|
||||
// Connection initialization function for reconnections
|
||||
initConnFunc func(context.Context, *Conn) error
|
||||
|
||||
onClose func() error
|
||||
}
|
||||
|
||||
func NewConn(netConn net.Conn) *Conn {
|
||||
return NewConnWithBufferSize(netConn, proto.DefaultBufferSize, proto.DefaultBufferSize)
|
||||
}
|
||||
|
||||
func NewConnWithBufferSize(netConn net.Conn, readBufSize, writeBufSize int) *Conn {
|
||||
now := time.Now()
|
||||
cn := &Conn{
|
||||
netConn: netConn,
|
||||
createdAt: time.Now(),
|
||||
createdAt: now,
|
||||
id: generateConnID(), // Generate unique ID for this connection
|
||||
stateMachine: NewConnStateMachine(),
|
||||
}
|
||||
cn.rd = proto.NewReader(netConn)
|
||||
cn.bw = bufio.NewWriter(netConn)
|
||||
|
||||
// Use specified buffer sizes, or fall back to 32KiB defaults if 0
|
||||
if readBufSize > 0 {
|
||||
cn.rd = proto.NewReaderSize(netConn, readBufSize)
|
||||
} else {
|
||||
cn.rd = proto.NewReader(netConn) // Uses 32KiB default
|
||||
}
|
||||
|
||||
if writeBufSize > 0 {
|
||||
cn.bw = bufio.NewWriterSize(netConn, writeBufSize)
|
||||
} else {
|
||||
cn.bw = bufio.NewWriterSize(netConn, proto.DefaultBufferSize)
|
||||
}
|
||||
|
||||
// Store netConn atomically for lock-free access using wrapper
|
||||
cn.netConnAtomic.Store(&atomicNetConn{conn: netConn})
|
||||
|
||||
cn.wr = proto.NewWriter(cn.bw)
|
||||
cn.SetUsedAt(time.Now())
|
||||
cn.SetUsedAt(now)
|
||||
// Initialize handoff state atomically
|
||||
initialHandoffState := &HandoffState{
|
||||
ShouldHandoff: false,
|
||||
Endpoint: "",
|
||||
SeqID: 0,
|
||||
}
|
||||
cn.handoffStateAtomic.Store(initialHandoffState)
|
||||
return cn
|
||||
}
|
||||
|
||||
func (cn *Conn) UsedAt() time.Time {
|
||||
unix := atomic.LoadInt64(&cn.usedAt)
|
||||
return time.Unix(unix, 0)
|
||||
return time.Unix(0, cn.usedAt.Load())
|
||||
}
|
||||
func (cn *Conn) SetUsedAt(tm time.Time) {
|
||||
cn.usedAt.Store(tm.UnixNano())
|
||||
}
|
||||
|
||||
func (cn *Conn) SetUsedAt(tm time.Time) {
|
||||
atomic.StoreInt64(&cn.usedAt, tm.Unix())
|
||||
func (cn *Conn) UsedAtNs() int64 {
|
||||
return cn.usedAt.Load()
|
||||
}
|
||||
func (cn *Conn) SetUsedAtNs(ns int64) {
|
||||
cn.usedAt.Store(ns)
|
||||
}
|
||||
|
||||
func (cn *Conn) LastPutAtNs() int64 {
|
||||
return cn.lastPutAt.Load()
|
||||
}
|
||||
func (cn *Conn) SetLastPutAtNs(ns int64) {
|
||||
cn.lastPutAt.Store(ns)
|
||||
}
|
||||
|
||||
// Backward-compatible wrapper methods for state machine
|
||||
// These maintain the existing API while using the new state machine internally
|
||||
|
||||
// CompareAndSwapUsable atomically compares and swaps the usable flag (lock-free).
|
||||
//
|
||||
// This is used by background operations (handoff, re-auth) to acquire exclusive
|
||||
// access to a connection. The operation sets usable to false, preventing the pool
|
||||
// from returning the connection to clients.
|
||||
//
|
||||
// Returns true if the swap was successful (old value matched), false otherwise.
|
||||
//
|
||||
// Implementation note: This is a compatibility wrapper around the state machine.
|
||||
// It checks if the current state is "usable" (IDLE or IN_USE) and transitions accordingly.
|
||||
// Deprecated: Use GetStateMachine().TryTransition() directly for better state management.
|
||||
func (cn *Conn) CompareAndSwapUsable(old, new bool) bool {
|
||||
currentState := cn.stateMachine.GetState()
|
||||
|
||||
// Check if current state matches the "old" usable value
|
||||
currentUsable := (currentState == StateIdle || currentState == StateInUse)
|
||||
if currentUsable != old {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we're trying to set to the same value, succeed immediately
|
||||
if old == new {
|
||||
return true
|
||||
}
|
||||
|
||||
// Transition based on new value
|
||||
if new {
|
||||
// Trying to make usable - transition from UNUSABLE to IDLE
|
||||
// This should only work from UNUSABLE or INITIALIZING states
|
||||
// Use predefined slice to avoid allocation
|
||||
_, err := cn.stateMachine.TryTransition(
|
||||
validFromInitializingOrUnusable,
|
||||
StateIdle,
|
||||
)
|
||||
return err == nil
|
||||
}
|
||||
// Trying to make unusable - transition from IDLE to UNUSABLE
|
||||
// This is typically for acquiring the connection for background operations
|
||||
// Use predefined slice to avoid allocation
|
||||
_, err := cn.stateMachine.TryTransition(
|
||||
validFromIdle,
|
||||
StateUnusable,
|
||||
)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsUsable returns true if the connection is safe to use for new commands (lock-free).
|
||||
//
|
||||
// A connection is "usable" when it's in a stable state and can be returned to clients.
|
||||
// It becomes unusable during:
|
||||
// - Handoff operations (network connection replacement)
|
||||
// - Re-authentication (credential updates)
|
||||
// - Other background operations that need exclusive access
|
||||
//
|
||||
// Note: CREATED state is considered usable because new connections need to pass OnGet() hook
|
||||
// before initialization. The initialization happens after OnGet() in the client code.
|
||||
func (cn *Conn) IsUsable() bool {
|
||||
state := cn.stateMachine.GetState()
|
||||
// CREATED, IDLE, and IN_USE states are considered usable
|
||||
// CREATED: new connection, not yet initialized (will be initialized by client)
|
||||
// IDLE: initialized and ready to be acquired
|
||||
// IN_USE: usable but currently acquired by someone
|
||||
return state == StateCreated || state == StateIdle || state == StateInUse
|
||||
}
|
||||
|
||||
// SetUsable sets the usable flag for the connection (lock-free).
|
||||
//
|
||||
// Deprecated: Use GetStateMachine().Transition() directly for better state management.
|
||||
// This method is kept for backwards compatibility.
|
||||
//
|
||||
// This should be called to mark a connection as usable after initialization or
|
||||
// to release it after a background operation completes.
|
||||
//
|
||||
// Prefer CompareAndSwapUsable() when acquiring exclusive access to avoid race conditions.
|
||||
// Deprecated: Use GetStateMachine().Transition() directly for better state management.
|
||||
func (cn *Conn) SetUsable(usable bool) {
|
||||
if usable {
|
||||
// Transition to IDLE state (ready to be acquired)
|
||||
cn.stateMachine.Transition(StateIdle)
|
||||
} else {
|
||||
// Transition to UNUSABLE state (for background operations)
|
||||
cn.stateMachine.Transition(StateUnusable)
|
||||
}
|
||||
}
|
||||
|
||||
// IsInited returns true if the connection has been initialized.
|
||||
// This is a backward-compatible wrapper around the state machine.
|
||||
func (cn *Conn) IsInited() bool {
|
||||
state := cn.stateMachine.GetState()
|
||||
// Connection is initialized if it's in IDLE or any post-initialization state
|
||||
return state != StateCreated && state != StateInitializing && state != StateClosed
|
||||
}
|
||||
|
||||
// Used - State machine based implementation
|
||||
|
||||
// CompareAndSwapUsed atomically compares and swaps the used flag (lock-free).
|
||||
// This method is kept for backwards compatibility.
|
||||
//
|
||||
// This is the preferred method for acquiring a connection from the pool, as it
|
||||
// ensures that only one goroutine marks the connection as used.
|
||||
//
|
||||
// Implementation: Uses state machine transitions IDLE ⇄ IN_USE
|
||||
//
|
||||
// Returns true if the swap was successful (old value matched), false otherwise.
|
||||
// Deprecated: Use GetStateMachine().TryTransition() directly for better state management.
|
||||
func (cn *Conn) CompareAndSwapUsed(old, new bool) bool {
|
||||
if old == new {
|
||||
// No change needed
|
||||
currentState := cn.stateMachine.GetState()
|
||||
currentUsed := (currentState == StateInUse)
|
||||
return currentUsed == old
|
||||
}
|
||||
|
||||
if !old && new {
|
||||
// Acquiring: IDLE → IN_USE
|
||||
// Use predefined slice to avoid allocation
|
||||
_, err := cn.stateMachine.TryTransition(validFromCreatedOrIdle, StateInUse)
|
||||
return err == nil
|
||||
} else {
|
||||
// Releasing: IN_USE → IDLE
|
||||
// Use predefined slice to avoid allocation
|
||||
_, err := cn.stateMachine.TryTransition(validFromInUse, StateIdle)
|
||||
return err == nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsUsed returns true if the connection is currently in use (lock-free).
|
||||
//
|
||||
// Deprecated: Use GetStateMachine().GetState() == StateInUse directly for better clarity.
|
||||
// This method is kept for backwards compatibility.
|
||||
//
|
||||
// A connection is "used" when it has been retrieved from the pool and is
|
||||
// actively processing a command. Background operations (like re-auth) should
|
||||
// wait until the connection is not used before executing commands.
|
||||
func (cn *Conn) IsUsed() bool {
|
||||
return cn.stateMachine.GetState() == StateInUse
|
||||
}
|
||||
|
||||
// SetUsed sets the used flag for the connection (lock-free).
|
||||
//
|
||||
// This should be called when returning a connection to the pool (set to false)
|
||||
// or when a single-connection pool retrieves its connection (set to true).
|
||||
//
|
||||
// Prefer CompareAndSwapUsed() when acquiring from a multi-connection pool to
|
||||
// avoid race conditions.
|
||||
// Deprecated: Use GetStateMachine().Transition() directly for better state management.
|
||||
func (cn *Conn) SetUsed(val bool) {
|
||||
if val {
|
||||
cn.stateMachine.Transition(StateInUse)
|
||||
} else {
|
||||
cn.stateMachine.Transition(StateIdle)
|
||||
}
|
||||
}
|
||||
|
||||
// getNetConn returns the current network connection using atomic load (lock-free).
|
||||
// This is the fast path for accessing netConn without mutex overhead.
|
||||
func (cn *Conn) getNetConn() net.Conn {
|
||||
if v := cn.netConnAtomic.Load(); v != nil {
|
||||
if wrapper, ok := v.(*atomicNetConn); ok {
|
||||
return wrapper.conn
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setNetConn stores the network connection atomically (lock-free).
|
||||
// This is used for the fast path of connection replacement.
|
||||
func (cn *Conn) setNetConn(netConn net.Conn) {
|
||||
cn.netConnAtomic.Store(&atomicNetConn{conn: netConn})
|
||||
}
|
||||
|
||||
// Handoff state management - atomic access to handoff metadata
|
||||
|
||||
// ShouldHandoff returns true if connection needs handoff (lock-free).
|
||||
func (cn *Conn) ShouldHandoff() bool {
|
||||
if v := cn.handoffStateAtomic.Load(); v != nil {
|
||||
return v.(*HandoffState).ShouldHandoff
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetHandoffEndpoint returns the new endpoint for handoff (lock-free).
|
||||
func (cn *Conn) GetHandoffEndpoint() string {
|
||||
if v := cn.handoffStateAtomic.Load(); v != nil {
|
||||
return v.(*HandoffState).Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetMovingSeqID returns the sequence ID from the MOVING notification (lock-free).
|
||||
func (cn *Conn) GetMovingSeqID() int64 {
|
||||
if v := cn.handoffStateAtomic.Load(); v != nil {
|
||||
return v.(*HandoffState).SeqID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetHandoffInfo returns all handoff information atomically (lock-free).
|
||||
// This method prevents race conditions by returning all handoff state in a single atomic operation.
|
||||
// Returns (shouldHandoff, endpoint, seqID).
|
||||
func (cn *Conn) GetHandoffInfo() (bool, string, int64) {
|
||||
if v := cn.handoffStateAtomic.Load(); v != nil {
|
||||
state := v.(*HandoffState)
|
||||
return state.ShouldHandoff, state.Endpoint, state.SeqID
|
||||
}
|
||||
return false, "", 0
|
||||
}
|
||||
|
||||
// HandoffRetries returns the current handoff retry count (lock-free).
|
||||
func (cn *Conn) HandoffRetries() int {
|
||||
return int(cn.handoffRetriesAtomic.Load())
|
||||
}
|
||||
|
||||
// IncrementAndGetHandoffRetries atomically increments and returns handoff retries (lock-free).
|
||||
func (cn *Conn) IncrementAndGetHandoffRetries(n int) int {
|
||||
return int(cn.handoffRetriesAtomic.Add(uint32(n)))
|
||||
}
|
||||
|
||||
// IsPooled returns true if the connection is managed by a pool and will be pooled on Put.
|
||||
func (cn *Conn) IsPooled() bool {
|
||||
return cn.pooled
|
||||
}
|
||||
|
||||
// IsPubSub returns true if the connection is used for PubSub.
|
||||
func (cn *Conn) IsPubSub() bool {
|
||||
return cn.pubsub
|
||||
}
|
||||
|
||||
// SetRelaxedTimeout sets relaxed timeouts for this connection during maintenanceNotifications upgrades.
|
||||
// These timeouts will be used for all subsequent commands until the deadline expires.
|
||||
// Uses atomic operations for lock-free access.
|
||||
func (cn *Conn) SetRelaxedTimeout(readTimeout, writeTimeout time.Duration) {
|
||||
cn.relaxedCounter.Add(1)
|
||||
cn.relaxedReadTimeoutNs.Store(int64(readTimeout))
|
||||
cn.relaxedWriteTimeoutNs.Store(int64(writeTimeout))
|
||||
}
|
||||
|
||||
// SetRelaxedTimeoutWithDeadline sets relaxed timeouts with an expiration deadline.
|
||||
// After the deadline, timeouts automatically revert to normal values.
|
||||
// Uses atomic operations for lock-free access.
|
||||
func (cn *Conn) SetRelaxedTimeoutWithDeadline(readTimeout, writeTimeout time.Duration, deadline time.Time) {
|
||||
cn.SetRelaxedTimeout(readTimeout, writeTimeout)
|
||||
cn.relaxedDeadlineNs.Store(deadline.UnixNano())
|
||||
}
|
||||
|
||||
// ClearRelaxedTimeout removes relaxed timeouts, returning to normal timeout behavior.
|
||||
// Uses atomic operations for lock-free access.
|
||||
func (cn *Conn) ClearRelaxedTimeout() {
|
||||
// Atomically decrement counter and check if we should clear
|
||||
newCount := cn.relaxedCounter.Add(-1)
|
||||
deadlineNs := cn.relaxedDeadlineNs.Load()
|
||||
if newCount <= 0 && (deadlineNs == 0 || time.Now().UnixNano() >= deadlineNs) {
|
||||
// Use atomic load to get current value for CAS to avoid stale value race
|
||||
current := cn.relaxedCounter.Load()
|
||||
if current <= 0 && cn.relaxedCounter.CompareAndSwap(current, 0) {
|
||||
cn.clearRelaxedTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cn *Conn) clearRelaxedTimeout() {
|
||||
cn.relaxedReadTimeoutNs.Store(0)
|
||||
cn.relaxedWriteTimeoutNs.Store(0)
|
||||
cn.relaxedDeadlineNs.Store(0)
|
||||
cn.relaxedCounter.Store(0)
|
||||
}
|
||||
|
||||
// HasRelaxedTimeout returns true if relaxed timeouts are currently active on this connection.
|
||||
// This checks both the timeout values and the deadline (if set).
|
||||
// Uses atomic operations for lock-free access.
|
||||
func (cn *Conn) HasRelaxedTimeout() bool {
|
||||
// Fast path: no relaxed timeouts are set
|
||||
if cn.relaxedCounter.Load() <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
readTimeoutNs := cn.relaxedReadTimeoutNs.Load()
|
||||
writeTimeoutNs := cn.relaxedWriteTimeoutNs.Load()
|
||||
|
||||
// If no relaxed timeouts are set, return false
|
||||
if readTimeoutNs <= 0 && writeTimeoutNs <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
deadlineNs := cn.relaxedDeadlineNs.Load()
|
||||
// If no deadline is set, relaxed timeouts are active
|
||||
if deadlineNs == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// If deadline is set, check if it's still in the future
|
||||
return time.Now().UnixNano() < deadlineNs
|
||||
}
|
||||
|
||||
// getEffectiveReadTimeout returns the timeout to use for read operations.
|
||||
// If relaxed timeout is set and not expired, it takes precedence over the provided timeout.
|
||||
// This method automatically clears expired relaxed timeouts using atomic operations.
|
||||
func (cn *Conn) getEffectiveReadTimeout(normalTimeout time.Duration) time.Duration {
|
||||
readTimeoutNs := cn.relaxedReadTimeoutNs.Load()
|
||||
|
||||
// Fast path: no relaxed timeout set
|
||||
if readTimeoutNs <= 0 {
|
||||
return normalTimeout
|
||||
}
|
||||
|
||||
deadlineNs := cn.relaxedDeadlineNs.Load()
|
||||
// If no deadline is set, use relaxed timeout
|
||||
if deadlineNs == 0 {
|
||||
return time.Duration(readTimeoutNs)
|
||||
}
|
||||
|
||||
// Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks)
|
||||
nowNs := getCachedTimeNs()
|
||||
// Check if deadline has passed
|
||||
if nowNs < deadlineNs {
|
||||
// Deadline is in the future, use relaxed timeout
|
||||
return time.Duration(readTimeoutNs)
|
||||
} else {
|
||||
// Deadline has passed, clear relaxed timeouts atomically and use normal timeout
|
||||
newCount := cn.relaxedCounter.Add(-1)
|
||||
if newCount <= 0 {
|
||||
internal.Logger.Printf(context.Background(), logs.UnrelaxedTimeoutAfterDeadline(cn.GetID()))
|
||||
cn.clearRelaxedTimeout()
|
||||
}
|
||||
return normalTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// getEffectiveWriteTimeout returns the timeout to use for write operations.
|
||||
// If relaxed timeout is set and not expired, it takes precedence over the provided timeout.
|
||||
// This method automatically clears expired relaxed timeouts using atomic operations.
|
||||
func (cn *Conn) getEffectiveWriteTimeout(normalTimeout time.Duration) time.Duration {
|
||||
writeTimeoutNs := cn.relaxedWriteTimeoutNs.Load()
|
||||
|
||||
// Fast path: no relaxed timeout set
|
||||
if writeTimeoutNs <= 0 {
|
||||
return normalTimeout
|
||||
}
|
||||
|
||||
deadlineNs := cn.relaxedDeadlineNs.Load()
|
||||
// If no deadline is set, use relaxed timeout
|
||||
if deadlineNs == 0 {
|
||||
return time.Duration(writeTimeoutNs)
|
||||
}
|
||||
|
||||
// Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks)
|
||||
nowNs := getCachedTimeNs()
|
||||
// Check if deadline has passed
|
||||
if nowNs < deadlineNs {
|
||||
// Deadline is in the future, use relaxed timeout
|
||||
return time.Duration(writeTimeoutNs)
|
||||
} else {
|
||||
// Deadline has passed, clear relaxed timeouts atomically and use normal timeout
|
||||
newCount := cn.relaxedCounter.Add(-1)
|
||||
if newCount <= 0 {
|
||||
internal.Logger.Printf(context.Background(), logs.UnrelaxedTimeoutAfterDeadline(cn.GetID()))
|
||||
cn.clearRelaxedTimeout()
|
||||
}
|
||||
return normalTimeout
|
||||
}
|
||||
}
|
||||
|
||||
func (cn *Conn) SetOnClose(fn func() error) {
|
||||
cn.onClose = fn
|
||||
}
|
||||
|
||||
// SetInitConnFunc sets the connection initialization function to be called on reconnections.
|
||||
func (cn *Conn) SetInitConnFunc(fn func(context.Context, *Conn) error) {
|
||||
cn.initConnFunc = fn
|
||||
}
|
||||
|
||||
// ExecuteInitConn runs the stored connection initialization function if available.
|
||||
func (cn *Conn) ExecuteInitConn(ctx context.Context) error {
|
||||
if cn.initConnFunc != nil {
|
||||
return cn.initConnFunc(ctx, cn)
|
||||
}
|
||||
return fmt.Errorf("redis: no initConnFunc set for conn[%d]", cn.GetID())
|
||||
}
|
||||
|
||||
func (cn *Conn) SetNetConn(netConn net.Conn) {
|
||||
cn.netConn = netConn
|
||||
// Store the new connection atomically first (lock-free)
|
||||
cn.setNetConn(netConn)
|
||||
// Protect reader reset operations to avoid data races
|
||||
// Use write lock since we're modifying the reader state
|
||||
cn.readerMu.Lock()
|
||||
cn.rd.Reset(netConn)
|
||||
cn.readerMu.Unlock()
|
||||
|
||||
cn.bw.Reset(netConn)
|
||||
}
|
||||
|
||||
// GetNetConn safely returns the current network connection using atomic load (lock-free).
|
||||
// This method is used by the pool for health checks and provides better performance.
|
||||
func (cn *Conn) GetNetConn() net.Conn {
|
||||
return cn.getNetConn()
|
||||
}
|
||||
|
||||
// SetNetConnAndInitConn replaces the underlying connection and executes the initialization.
|
||||
// This method ensures only one initialization can happen at a time by using atomic state transitions.
|
||||
// If another goroutine is currently initializing, this will wait for it to complete.
|
||||
func (cn *Conn) SetNetConnAndInitConn(ctx context.Context, netConn net.Conn) error {
|
||||
// Wait for and transition to INITIALIZING state - this prevents concurrent initializations
|
||||
// Valid from states: CREATED (first init), IDLE (reconnect), UNUSABLE (handoff/reauth)
|
||||
// If another goroutine is initializing, we'll wait for it to finish
|
||||
// if the context has a deadline, use that, otherwise use the connection read (relaxed) timeout
|
||||
// which should be set during handoff. If it is not set, use a 5 second default
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
deadline = time.Now().Add(cn.getEffectiveReadTimeout(5 * time.Second))
|
||||
}
|
||||
waitCtx, cancel := context.WithDeadline(ctx, deadline)
|
||||
defer cancel()
|
||||
// Use predefined slice to avoid allocation
|
||||
finalState, err := cn.stateMachine.AwaitAndTransition(
|
||||
waitCtx,
|
||||
validFromCreatedIdleOrUnusable,
|
||||
StateInitializing,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot initialize connection from state %s: %w", finalState, err)
|
||||
}
|
||||
|
||||
// Replace the underlying connection
|
||||
cn.SetNetConn(netConn)
|
||||
|
||||
// Execute initialization
|
||||
// NOTE: ExecuteInitConn (via baseClient.initConn) will transition to IDLE on success
|
||||
// or CLOSED on failure. We don't need to do it here.
|
||||
// NOTE: Initconn returns conn in IDLE state
|
||||
initErr := cn.ExecuteInitConn(ctx)
|
||||
if initErr != nil {
|
||||
// ExecuteInitConn already transitioned to CLOSED, just return the error
|
||||
return initErr
|
||||
}
|
||||
|
||||
// ExecuteInitConn already transitioned to IDLE
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkForHandoff marks the connection for handoff due to MOVING notification.
|
||||
// Returns an error if the connection is already marked for handoff.
|
||||
// Note: This only sets metadata - the connection state is not changed until OnPut.
|
||||
// This allows the current user to finish using the connection before handoff.
|
||||
func (cn *Conn) MarkForHandoff(newEndpoint string, seqID int64) error {
|
||||
// Check if already marked for handoff
|
||||
if cn.ShouldHandoff() {
|
||||
return errAlreadyMarkedForHandoff
|
||||
}
|
||||
|
||||
// Set handoff metadata atomically
|
||||
cn.handoffStateAtomic.Store(&HandoffState{
|
||||
ShouldHandoff: true,
|
||||
Endpoint: newEndpoint,
|
||||
SeqID: seqID,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkQueuedForHandoff marks the connection as queued for handoff processing.
|
||||
// This makes the connection unusable until handoff completes.
|
||||
// This is called from OnPut hook, where the connection is typically in IN_USE state.
|
||||
// The pool will preserve the UNUSABLE state and not overwrite it with IDLE.
|
||||
func (cn *Conn) MarkQueuedForHandoff() error {
|
||||
// Get current handoff state
|
||||
currentState := cn.handoffStateAtomic.Load()
|
||||
if currentState == nil {
|
||||
return errNotMarkedForHandoff
|
||||
}
|
||||
|
||||
state := currentState.(*HandoffState)
|
||||
if !state.ShouldHandoff {
|
||||
return errNotMarkedForHandoff
|
||||
}
|
||||
|
||||
// Create new state with ShouldHandoff=false but preserve endpoint and seqID
|
||||
// This prevents the connection from being queued multiple times while still
|
||||
// allowing the worker to access the handoff metadata
|
||||
newState := &HandoffState{
|
||||
ShouldHandoff: false,
|
||||
Endpoint: state.Endpoint, // Preserve endpoint for handoff processing
|
||||
SeqID: state.SeqID, // Preserve seqID for handoff processing
|
||||
}
|
||||
|
||||
// Atomic compare-and-swap to update state
|
||||
if !cn.handoffStateAtomic.CompareAndSwap(currentState, newState) {
|
||||
// State changed between load and CAS - retry or return error
|
||||
return errHandoffStateChanged
|
||||
}
|
||||
|
||||
// Transition to UNUSABLE from IN_USE (normal flow), IDLE (edge cases), or CREATED (tests/uninitialized)
|
||||
// The connection is typically in IN_USE state when OnPut is called (normal Put flow)
|
||||
// But in some edge cases or tests, it might be in IDLE or CREATED state
|
||||
// The pool will detect this state change and preserve it (not overwrite with IDLE)
|
||||
// Use predefined slice to avoid allocation
|
||||
finalState, err := cn.stateMachine.TryTransition(validFromCreatedInUseOrIdle, StateUnusable)
|
||||
if err != nil {
|
||||
// Check if already in UNUSABLE state (race condition or retry)
|
||||
// ShouldHandoff should be false now, but check just in case
|
||||
if finalState == StateUnusable && !cn.ShouldHandoff() {
|
||||
// Already unusable - this is fine, keep the new handoff state
|
||||
return nil
|
||||
}
|
||||
// Restore the original state if transition fails for other reasons
|
||||
cn.handoffStateAtomic.Store(currentState)
|
||||
return fmt.Errorf("failed to mark connection as unusable: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID returns the unique identifier for this connection.
|
||||
func (cn *Conn) GetID() uint64 {
|
||||
return cn.id
|
||||
}
|
||||
|
||||
// GetStateMachine returns the connection's state machine for advanced state management.
|
||||
// This is primarily used by internal packages like maintnotifications for handoff processing.
|
||||
func (cn *Conn) GetStateMachine() *ConnStateMachine {
|
||||
return cn.stateMachine
|
||||
}
|
||||
|
||||
// TryAcquire attempts to acquire the connection for use.
|
||||
// This is an optimized inline method for the hot path (Get operation).
|
||||
//
|
||||
// It tries to transition from IDLE -> IN_USE or CREATED -> CREATED.
|
||||
// Returns true if the connection was successfully acquired, false otherwise.
|
||||
// The CREATED->CREATED is done so we can keep the state correct for later
|
||||
// initialization of the connection in initConn.
|
||||
//
|
||||
// Performance: This is faster than calling GetStateMachine() + TryTransitionFast()
|
||||
//
|
||||
// NOTE: We directly access cn.stateMachine.state here instead of using the state machine's
|
||||
// methods. This breaks encapsulation but is necessary for performance.
|
||||
// The IDLE->IN_USE and CREATED->CREATED transitions don't need
|
||||
// waiter notification, and benchmarks show 1-3% improvement. If the state machine ever
|
||||
// needs to notify waiters on these transitions, update this to use TryTransitionFast().
|
||||
func (cn *Conn) TryAcquire() bool {
|
||||
// The || operator short-circuits, so only 1 CAS in the common case
|
||||
return cn.stateMachine.state.CompareAndSwap(uint32(StateIdle), uint32(StateInUse)) ||
|
||||
cn.stateMachine.state.CompareAndSwap(uint32(StateCreated), uint32(StateCreated))
|
||||
}
|
||||
|
||||
// Release releases the connection back to the pool.
|
||||
// This is an optimized inline method for the hot path (Put operation).
|
||||
//
|
||||
// It tries to transition from IN_USE -> IDLE.
|
||||
// Returns true if the connection was successfully released, false otherwise.
|
||||
//
|
||||
// Performance: This is faster than calling GetStateMachine() + TryTransitionFast().
|
||||
//
|
||||
// NOTE: We directly access cn.stateMachine.state here instead of using the state machine's
|
||||
// methods. This breaks encapsulation but is necessary for performance.
|
||||
// If the state machine ever needs to notify waiters
|
||||
// on this transition, update this to use TryTransitionFast().
|
||||
func (cn *Conn) Release() bool {
|
||||
// Inline the hot path - single CAS operation
|
||||
return cn.stateMachine.state.CompareAndSwap(uint32(StateInUse), uint32(StateIdle))
|
||||
}
|
||||
|
||||
// ClearHandoffState clears the handoff state after successful handoff.
|
||||
// Makes the connection usable again.
|
||||
func (cn *Conn) ClearHandoffState() {
|
||||
// Clear handoff metadata
|
||||
cn.handoffStateAtomic.Store(&HandoffState{
|
||||
ShouldHandoff: false,
|
||||
Endpoint: "",
|
||||
SeqID: 0,
|
||||
})
|
||||
|
||||
// Reset retry counter
|
||||
cn.handoffRetriesAtomic.Store(0)
|
||||
|
||||
// Mark connection as usable again
|
||||
// Use state machine directly instead of deprecated SetUsable
|
||||
// probably done by initConn
|
||||
cn.stateMachine.Transition(StateIdle)
|
||||
}
|
||||
|
||||
// HasBufferedData safely checks if the connection has buffered data.
|
||||
// This method is used to avoid data races when checking for push notifications.
|
||||
func (cn *Conn) HasBufferedData() bool {
|
||||
// Use read lock for concurrent access to reader state
|
||||
cn.readerMu.RLock()
|
||||
defer cn.readerMu.RUnlock()
|
||||
return cn.rd.Buffered() > 0
|
||||
}
|
||||
|
||||
// PeekReplyTypeSafe safely peeks at the reply type.
|
||||
// This method is used to avoid data races when checking for push notifications.
|
||||
func (cn *Conn) PeekReplyTypeSafe() (byte, error) {
|
||||
// Use read lock for concurrent access to reader state
|
||||
cn.readerMu.RLock()
|
||||
defer cn.readerMu.RUnlock()
|
||||
|
||||
if cn.rd.Buffered() <= 0 {
|
||||
return 0, fmt.Errorf("redis: can't peek reply type, no data available")
|
||||
}
|
||||
return cn.rd.PeekReplyType()
|
||||
}
|
||||
|
||||
func (cn *Conn) Write(b []byte) (int, error) {
|
||||
return cn.netConn.Write(b)
|
||||
// Lock-free netConn access for better performance
|
||||
if netConn := cn.getNetConn(); netConn != nil {
|
||||
return netConn.Write(b)
|
||||
}
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
|
||||
func (cn *Conn) RemoteAddr() net.Addr {
|
||||
if cn.netConn != nil {
|
||||
return cn.netConn.RemoteAddr()
|
||||
// Lock-free netConn access for better performance
|
||||
if netConn := cn.getNetConn(); netConn != nil {
|
||||
return netConn.RemoteAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -67,7 +806,16 @@ func (cn *Conn) WithReader(
|
||||
ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error,
|
||||
) error {
|
||||
if timeout >= 0 {
|
||||
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||
// Use relaxed timeout if set, otherwise use provided timeout
|
||||
effectiveTimeout := cn.getEffectiveReadTimeout(timeout)
|
||||
|
||||
// Get the connection directly from atomic storage
|
||||
netConn := cn.getNetConn()
|
||||
if netConn == nil {
|
||||
return errConnectionNotAvailable
|
||||
}
|
||||
|
||||
if err := netConn.SetReadDeadline(cn.deadline(ctx, effectiveTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -78,13 +826,25 @@ func (cn *Conn) WithWriter(
|
||||
ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error,
|
||||
) error {
|
||||
if timeout >= 0 {
|
||||
if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||
return err
|
||||
// Use relaxed timeout if set, otherwise use provided timeout
|
||||
effectiveTimeout := cn.getEffectiveWriteTimeout(timeout)
|
||||
|
||||
// Set write deadline on the connection
|
||||
if netConn := cn.getNetConn(); netConn != nil {
|
||||
if err := netConn.SetWriteDeadline(cn.deadline(ctx, effectiveTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Connection is not available - return preallocated error
|
||||
return errConnNotAvailableForWrite
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the buffered writer if needed, should not happen
|
||||
if cn.bw.Buffered() > 0 {
|
||||
cn.bw.Reset(cn.netConn)
|
||||
if netConn := cn.getNetConn(); netConn != nil {
|
||||
cn.bw.Reset(netConn)
|
||||
}
|
||||
}
|
||||
|
||||
if err := fn(cn.wr); err != nil {
|
||||
@@ -94,13 +854,47 @@ func (cn *Conn) WithWriter(
|
||||
return cn.bw.Flush()
|
||||
}
|
||||
|
||||
func (cn *Conn) Close() error {
|
||||
return cn.netConn.Close()
|
||||
func (cn *Conn) IsClosed() bool {
|
||||
return cn.closed.Load() || cn.stateMachine.GetState() == StateClosed
|
||||
}
|
||||
|
||||
func (cn *Conn) Close() error {
|
||||
cn.closed.Store(true)
|
||||
|
||||
// Transition to CLOSED state
|
||||
cn.stateMachine.Transition(StateClosed)
|
||||
|
||||
if cn.onClose != nil {
|
||||
// ignore error
|
||||
_ = cn.onClose()
|
||||
}
|
||||
|
||||
// Lock-free netConn access for better performance
|
||||
if netConn := cn.getNetConn(); netConn != nil {
|
||||
return netConn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaybeHasData tries to peek at the next byte in the socket without consuming it
|
||||
// This is used to check if there are push notifications available
|
||||
// Important: This will work on Linux, but not on Windows
|
||||
func (cn *Conn) MaybeHasData() bool {
|
||||
// Lock-free netConn access for better performance
|
||||
if netConn := cn.getNetConn(); netConn != nil {
|
||||
return maybeHasData(netConn)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// deadline computes the effective deadline time based on context and timeout.
|
||||
// It updates the usedAt timestamp to now.
|
||||
// Uses cached time to avoid expensive syscall (max 50ms staleness is acceptable for deadline calculation).
|
||||
func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time {
|
||||
tm := time.Now()
|
||||
cn.SetUsedAt(tm)
|
||||
// Use cached time for deadline calculation (called 2x per command: read + write)
|
||||
nowNs := getCachedTimeNs()
|
||||
cn.SetUsedAtNs(nowNs)
|
||||
tm := time.Unix(0, nowNs)
|
||||
|
||||
if timeout > 0 {
|
||||
tm = tm.Add(timeout)
|
||||
|
||||
+11
-1
@@ -12,6 +12,9 @@ import (
|
||||
|
||||
var errUnexpectedRead = errors.New("unexpected read from socket")
|
||||
|
||||
// connCheck checks if the connection is still alive and if there is data in the socket
|
||||
// it will try to peek at the next byte without consuming it since we may want to work with it
|
||||
// later on (e.g. push notifications)
|
||||
func connCheck(conn net.Conn) error {
|
||||
// Reset previous timeout.
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
@@ -29,7 +32,9 @@ func connCheck(conn net.Conn) error {
|
||||
|
||||
if err := rawConn.Read(func(fd uintptr) bool {
|
||||
var buf [1]byte
|
||||
n, err := syscall.Read(int(fd), buf[:])
|
||||
// Use MSG_PEEK to peek at data without consuming it
|
||||
n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT)
|
||||
|
||||
switch {
|
||||
case n == 0 && err == nil:
|
||||
sysErr = io.EOF
|
||||
@@ -47,3 +52,8 @@ func connCheck(conn net.Conn) error {
|
||||
|
||||
return sysErr
|
||||
}
|
||||
|
||||
// maybeHasData checks if there is data in the socket without consuming it
|
||||
func maybeHasData(conn net.Conn) bool {
|
||||
return connCheck(conn) == errUnexpectedRead
|
||||
}
|
||||
|
||||
+13
-2
@@ -2,8 +2,19 @@
|
||||
|
||||
package pool
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
func connCheck(conn net.Conn) error {
|
||||
// errUnexpectedRead is placeholder error variable for non-unix build constraints
|
||||
var errUnexpectedRead = errors.New("unexpected read from socket")
|
||||
|
||||
func connCheck(_ net.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// since we can't check for data on the socket, we just assume there is some
|
||||
func maybeHasData(_ net.Conn) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ConnState represents the connection state in the state machine.
|
||||
// States are designed to be lightweight and fast to check.
|
||||
//
|
||||
// State Transitions:
|
||||
// CREATED → INITIALIZING → IDLE ⇄ IN_USE
|
||||
// ↓
|
||||
// UNUSABLE (handoff/reauth)
|
||||
// ↓
|
||||
// IDLE/CLOSED
|
||||
type ConnState uint32
|
||||
|
||||
const (
|
||||
// StateCreated - Connection just created, not yet initialized
|
||||
StateCreated ConnState = iota
|
||||
|
||||
// StateInitializing - Connection initialization in progress
|
||||
StateInitializing
|
||||
|
||||
// StateIdle - Connection initialized and idle in pool, ready to be acquired
|
||||
StateIdle
|
||||
|
||||
// StateInUse - Connection actively processing a command (retrieved from pool)
|
||||
StateInUse
|
||||
|
||||
// StateUnusable - Connection temporarily unusable due to background operation
|
||||
// (handoff, reauth, etc.). Cannot be acquired from pool.
|
||||
StateUnusable
|
||||
|
||||
// StateClosed - Connection closed
|
||||
StateClosed
|
||||
)
|
||||
|
||||
// Predefined state slices to avoid allocations in hot paths
|
||||
var (
|
||||
validFromInUse = []ConnState{StateInUse}
|
||||
validFromCreatedOrIdle = []ConnState{StateCreated, StateIdle}
|
||||
validFromCreatedInUseOrIdle = []ConnState{StateCreated, StateInUse, StateIdle}
|
||||
// For AwaitAndTransition calls
|
||||
validFromCreatedIdleOrUnusable = []ConnState{StateCreated, StateIdle, StateUnusable}
|
||||
validFromIdle = []ConnState{StateIdle}
|
||||
// For CompareAndSwapUsable
|
||||
validFromInitializingOrUnusable = []ConnState{StateInitializing, StateUnusable}
|
||||
)
|
||||
|
||||
// Accessor functions for predefined slices to avoid allocations in external packages
|
||||
// These return the same slice instance, so they're zero-allocation
|
||||
|
||||
// ValidFromIdle returns a predefined slice containing only StateIdle.
|
||||
// Use this to avoid allocations when calling AwaitAndTransition or TryTransition.
|
||||
func ValidFromIdle() []ConnState {
|
||||
return validFromIdle
|
||||
}
|
||||
|
||||
// ValidFromCreatedIdleOrUnusable returns a predefined slice for initialization transitions.
|
||||
// Use this to avoid allocations when calling AwaitAndTransition or TryTransition.
|
||||
func ValidFromCreatedIdleOrUnusable() []ConnState {
|
||||
return validFromCreatedIdleOrUnusable
|
||||
}
|
||||
|
||||
// String returns a human-readable string representation of the state.
|
||||
func (s ConnState) String() string {
|
||||
switch s {
|
||||
case StateCreated:
|
||||
return "CREATED"
|
||||
case StateInitializing:
|
||||
return "INITIALIZING"
|
||||
case StateIdle:
|
||||
return "IDLE"
|
||||
case StateInUse:
|
||||
return "IN_USE"
|
||||
case StateUnusable:
|
||||
return "UNUSABLE"
|
||||
case StateClosed:
|
||||
return "CLOSED"
|
||||
default:
|
||||
return fmt.Sprintf("UNKNOWN(%d)", s)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidStateTransition is returned when a state transition is not allowed
|
||||
ErrInvalidStateTransition = errors.New("invalid state transition")
|
||||
|
||||
// ErrStateMachineClosed is returned when operating on a closed state machine
|
||||
ErrStateMachineClosed = errors.New("state machine is closed")
|
||||
|
||||
// ErrTimeout is returned when a state transition times out
|
||||
ErrTimeout = errors.New("state transition timeout")
|
||||
)
|
||||
|
||||
// waiter represents a goroutine waiting for a state transition.
|
||||
// Designed for minimal allocations and fast processing.
|
||||
type waiter struct {
|
||||
validStates map[ConnState]struct{} // States we're waiting for
|
||||
targetState ConnState // State to transition to
|
||||
done chan error // Signaled when transition completes or times out
|
||||
}
|
||||
|
||||
// ConnStateMachine manages connection state transitions with FIFO waiting queue.
|
||||
// Optimized for:
|
||||
// - Lock-free reads (hot path)
|
||||
// - Minimal allocations
|
||||
// - Fast state transitions
|
||||
// - FIFO fairness for waiters
|
||||
// Note: Handoff metadata (endpoint, seqID, retries) is managed separately in the Conn struct.
|
||||
type ConnStateMachine struct {
|
||||
// Current state - atomic for lock-free reads
|
||||
state atomic.Uint32
|
||||
|
||||
// FIFO queue for waiters - only locked during waiter add/remove/notify
|
||||
mu sync.Mutex
|
||||
waiters *list.List // List of *waiter
|
||||
waiterCount atomic.Int32 // Fast lock-free check for waiters (avoids mutex in hot path)
|
||||
}
|
||||
|
||||
// NewConnStateMachine creates a new connection state machine.
|
||||
// Initial state is StateCreated.
|
||||
func NewConnStateMachine() *ConnStateMachine {
|
||||
sm := &ConnStateMachine{
|
||||
waiters: list.New(),
|
||||
}
|
||||
sm.state.Store(uint32(StateCreated))
|
||||
return sm
|
||||
}
|
||||
|
||||
// GetState returns the current state (lock-free read).
|
||||
// This is the hot path - optimized for zero allocations and minimal overhead.
|
||||
// Note: Zero allocations applies to state reads; converting the returned state to a string
|
||||
// (via String()) may allocate if the state is unknown.
|
||||
func (sm *ConnStateMachine) GetState() ConnState {
|
||||
return ConnState(sm.state.Load())
|
||||
}
|
||||
|
||||
// TryTransitionFast is an optimized version for the hot path (Get/Put operations).
|
||||
// It only handles simple state transitions without waiter notification.
|
||||
// This is safe because:
|
||||
// 1. Get/Put don't need to wait for state changes
|
||||
// 2. Background operations (handoff/reauth) use UNUSABLE state, which this won't match
|
||||
// 3. If a background operation is in progress (state is UNUSABLE), this fails fast
|
||||
//
|
||||
// Returns true if transition succeeded, false otherwise.
|
||||
// Use this for performance-critical paths where you don't need error details.
|
||||
//
|
||||
// Performance: Single CAS operation - as fast as the old atomic bool!
|
||||
// For multiple from states, use: sm.TryTransitionFast(State1, Target) || sm.TryTransitionFast(State2, Target)
|
||||
// The || operator short-circuits, so only 1 CAS is executed in the common case.
|
||||
func (sm *ConnStateMachine) TryTransitionFast(fromState, targetState ConnState) bool {
|
||||
return sm.state.CompareAndSwap(uint32(fromState), uint32(targetState))
|
||||
}
|
||||
|
||||
// TryTransition attempts an immediate state transition without waiting.
|
||||
// Returns the current state after the transition attempt and an error if the transition failed.
|
||||
// The returned state is the CURRENT state (after the attempt), not the previous state.
|
||||
// This is faster than AwaitAndTransition when you don't need to wait.
|
||||
// Uses compare-and-swap to atomically transition, preventing concurrent transitions.
|
||||
// This method does NOT wait - it fails immediately if the transition cannot be performed.
|
||||
//
|
||||
// Performance: Zero allocations on success path (hot path).
|
||||
func (sm *ConnStateMachine) TryTransition(validFromStates []ConnState, targetState ConnState) (ConnState, error) {
|
||||
// Try each valid from state with CAS
|
||||
// This ensures only ONE goroutine can successfully transition at a time
|
||||
for _, fromState := range validFromStates {
|
||||
// Try to atomically swap from fromState to targetState
|
||||
// If successful, we won the race and can proceed
|
||||
if sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) {
|
||||
// Success! We transitioned atomically
|
||||
// Hot path optimization: only check for waiters if transition succeeded
|
||||
// This avoids atomic load on every Get/Put when no waiters exist
|
||||
if sm.waiterCount.Load() > 0 {
|
||||
sm.notifyWaiters()
|
||||
}
|
||||
return targetState, nil
|
||||
}
|
||||
}
|
||||
|
||||
// All CAS attempts failed - state is not valid for this transition
|
||||
// Return the current state so caller can decide what to do
|
||||
// Note: This error path allocates, but it's the exceptional case
|
||||
currentState := sm.GetState()
|
||||
return currentState, fmt.Errorf("%w: cannot transition from %s to %s (valid from: %v)",
|
||||
ErrInvalidStateTransition, currentState, targetState, validFromStates)
|
||||
}
|
||||
|
||||
// Transition unconditionally transitions to the target state.
|
||||
// Use with caution - prefer AwaitAndTransition or TryTransition for safety.
|
||||
// This is useful for error paths or when you know the transition is valid.
|
||||
func (sm *ConnStateMachine) Transition(targetState ConnState) {
|
||||
sm.state.Store(uint32(targetState))
|
||||
sm.notifyWaiters()
|
||||
}
|
||||
|
||||
// AwaitAndTransition waits for the connection to reach one of the valid states,
|
||||
// then atomically transitions to the target state.
|
||||
// Returns the current state after the transition attempt and an error if the operation failed.
|
||||
// The returned state is the CURRENT state (after the attempt), not the previous state.
|
||||
// Returns error if timeout expires or context is cancelled.
|
||||
//
|
||||
// This method implements FIFO fairness - the first caller to wait gets priority
|
||||
// when the state becomes available.
|
||||
//
|
||||
// Performance notes:
|
||||
// - If already in a valid state, this is very fast (no allocation, no waiting)
|
||||
// - If waiting is required, allocates one waiter struct and one channel
|
||||
func (sm *ConnStateMachine) AwaitAndTransition(
|
||||
ctx context.Context,
|
||||
validFromStates []ConnState,
|
||||
targetState ConnState,
|
||||
) (ConnState, error) {
|
||||
// Fast path: try immediate transition with CAS to prevent race conditions
|
||||
// BUT: only if there are no waiters in the queue (to maintain FIFO ordering)
|
||||
if sm.waiterCount.Load() == 0 {
|
||||
for _, fromState := range validFromStates {
|
||||
// Check if we're already in target state
|
||||
if fromState == targetState && sm.GetState() == targetState {
|
||||
return targetState, nil
|
||||
}
|
||||
|
||||
// Try to atomically swap from fromState to targetState
|
||||
if sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) {
|
||||
// Success! We transitioned atomically
|
||||
sm.notifyWaiters()
|
||||
return targetState, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fast path failed - check if we should wait or fail
|
||||
currentState := sm.GetState()
|
||||
|
||||
// Check if closed
|
||||
if currentState == StateClosed {
|
||||
return currentState, ErrStateMachineClosed
|
||||
}
|
||||
|
||||
// Slow path: need to wait for state change
|
||||
// Create waiter with valid states map for fast lookup
|
||||
validStatesMap := make(map[ConnState]struct{}, len(validFromStates))
|
||||
for _, s := range validFromStates {
|
||||
validStatesMap[s] = struct{}{}
|
||||
}
|
||||
|
||||
w := &waiter{
|
||||
validStates: validStatesMap,
|
||||
targetState: targetState,
|
||||
done: make(chan error, 1), // Buffered to avoid goroutine leak
|
||||
}
|
||||
|
||||
// Add to FIFO queue
|
||||
sm.mu.Lock()
|
||||
elem := sm.waiters.PushBack(w)
|
||||
sm.waiterCount.Add(1)
|
||||
sm.mu.Unlock()
|
||||
|
||||
// Wait for state change or timeout
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Timeout or cancellation - remove from queue
|
||||
sm.mu.Lock()
|
||||
sm.waiters.Remove(elem)
|
||||
sm.waiterCount.Add(-1)
|
||||
sm.mu.Unlock()
|
||||
return sm.GetState(), ctx.Err()
|
||||
case err := <-w.done:
|
||||
// Transition completed (or failed)
|
||||
// Note: waiterCount is decremented either in notifyWaiters (when the waiter is notified and removed)
|
||||
// or here (on timeout/cancellation).
|
||||
return sm.GetState(), err
|
||||
}
|
||||
}
|
||||
|
||||
// notifyWaiters checks if any waiters can proceed and notifies them in FIFO order.
|
||||
// This is called after every state transition.
|
||||
func (sm *ConnStateMachine) notifyWaiters() {
|
||||
// Fast path: check atomic counter without acquiring lock
|
||||
// This eliminates mutex overhead in the common case (no waiters)
|
||||
if sm.waiterCount.Load() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
// Double-check after acquiring lock (waiters might have been processed)
|
||||
if sm.waiters.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Process waiters in FIFO order until no more can be processed
|
||||
// We loop instead of recursing to avoid stack overflow and mutex issues
|
||||
for {
|
||||
processed := false
|
||||
|
||||
// Find the first waiter that can proceed
|
||||
for elem := sm.waiters.Front(); elem != nil; elem = elem.Next() {
|
||||
w := elem.Value.(*waiter)
|
||||
|
||||
// Read current state inside the loop to get the latest value
|
||||
currentState := sm.GetState()
|
||||
|
||||
// Check if current state is valid for this waiter
|
||||
if _, valid := w.validStates[currentState]; valid {
|
||||
// Remove from queue first
|
||||
sm.waiters.Remove(elem)
|
||||
sm.waiterCount.Add(-1)
|
||||
|
||||
// Use CAS to ensure state hasn't changed since we checked
|
||||
// This prevents race condition where another thread changes state
|
||||
// between our check and our transition
|
||||
if sm.state.CompareAndSwap(uint32(currentState), uint32(w.targetState)) {
|
||||
// Successfully transitioned - notify waiter
|
||||
w.done <- nil
|
||||
processed = true
|
||||
break
|
||||
} else {
|
||||
// State changed - re-add waiter to front of queue to maintain FIFO ordering
|
||||
// This waiter was first in line and should retain priority
|
||||
sm.waiters.PushFront(w)
|
||||
sm.waiterCount.Add(1)
|
||||
// Continue to next iteration to re-read state
|
||||
processed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't process any waiter, we're done
|
||||
if !processed {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// PoolHook defines the interface for connection lifecycle hooks.
|
||||
type PoolHook interface {
|
||||
// OnGet is called when a connection is retrieved from the pool.
|
||||
// It can modify the connection or return an error to prevent its use.
|
||||
// The accept flag can be used to prevent the connection from being used.
|
||||
// On Accept = false the connection is rejected and returned to the pool.
|
||||
// The error can be used to prevent the connection from being used and returned to the pool.
|
||||
// On Errors, the connection is removed from the pool.
|
||||
// It has isNewConn flag to indicate if this is a new connection (rather than idle from the pool)
|
||||
// The flag can be used for gathering metrics on pool hit/miss ratio.
|
||||
OnGet(ctx context.Context, conn *Conn, isNewConn bool) (accept bool, err error)
|
||||
|
||||
// OnPut is called when a connection is returned to the pool.
|
||||
// It returns whether the connection should be pooled and whether it should be removed.
|
||||
OnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error)
|
||||
|
||||
// OnRemove is called when a connection is removed from the pool.
|
||||
// This happens when:
|
||||
// - Connection fails health check
|
||||
// - Connection exceeds max lifetime
|
||||
// - Pool is being closed
|
||||
// - Connection encounters an error
|
||||
// Implementations should clean up any per-connection state.
|
||||
// The reason parameter indicates why the connection was removed.
|
||||
OnRemove(ctx context.Context, conn *Conn, reason error)
|
||||
}
|
||||
|
||||
// PoolHookManager manages multiple pool hooks.
|
||||
type PoolHookManager struct {
|
||||
hooks []PoolHook
|
||||
hooksMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPoolHookManager creates a new pool hook manager.
|
||||
func NewPoolHookManager() *PoolHookManager {
|
||||
return &PoolHookManager{
|
||||
hooks: make([]PoolHook, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddHook adds a pool hook to the manager.
|
||||
// Hooks are called in the order they were added.
|
||||
func (phm *PoolHookManager) AddHook(hook PoolHook) {
|
||||
phm.hooksMu.Lock()
|
||||
defer phm.hooksMu.Unlock()
|
||||
phm.hooks = append(phm.hooks, hook)
|
||||
}
|
||||
|
||||
// RemoveHook removes a pool hook from the manager.
|
||||
func (phm *PoolHookManager) RemoveHook(hook PoolHook) {
|
||||
phm.hooksMu.Lock()
|
||||
defer phm.hooksMu.Unlock()
|
||||
|
||||
for i, h := range phm.hooks {
|
||||
if h == hook {
|
||||
// Remove hook by swapping with last element and truncating
|
||||
phm.hooks[i] = phm.hooks[len(phm.hooks)-1]
|
||||
phm.hooks = phm.hooks[:len(phm.hooks)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessOnGet calls all OnGet hooks in order.
|
||||
// If any hook returns an error, processing stops and the error is returned.
|
||||
func (phm *PoolHookManager) ProcessOnGet(ctx context.Context, conn *Conn, isNewConn bool) (acceptConn bool, err error) {
|
||||
// Copy slice reference while holding lock (fast)
|
||||
phm.hooksMu.RLock()
|
||||
hooks := phm.hooks
|
||||
phm.hooksMu.RUnlock()
|
||||
|
||||
// Call hooks without holding lock (slow operations)
|
||||
for _, hook := range hooks {
|
||||
acceptConn, err := hook.OnGet(ctx, conn, isNewConn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !acceptConn {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ProcessOnPut calls all OnPut hooks in order.
|
||||
// The first hook that returns shouldRemove=true or shouldPool=false will stop processing.
|
||||
func (phm *PoolHookManager) ProcessOnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error) {
|
||||
// Copy slice reference while holding lock (fast)
|
||||
phm.hooksMu.RLock()
|
||||
hooks := phm.hooks
|
||||
phm.hooksMu.RUnlock()
|
||||
|
||||
shouldPool = true // Default to pooling the connection
|
||||
|
||||
// Call hooks without holding lock (slow operations)
|
||||
for _, hook := range hooks {
|
||||
hookShouldPool, hookShouldRemove, hookErr := hook.OnPut(ctx, conn)
|
||||
|
||||
if hookErr != nil {
|
||||
return false, true, hookErr
|
||||
}
|
||||
|
||||
// If any hook says to remove or not pool, respect that decision
|
||||
if hookShouldRemove {
|
||||
return false, true, nil
|
||||
}
|
||||
|
||||
if !hookShouldPool {
|
||||
shouldPool = false
|
||||
}
|
||||
}
|
||||
|
||||
return shouldPool, false, nil
|
||||
}
|
||||
|
||||
// ProcessOnRemove calls all OnRemove hooks in order.
|
||||
func (phm *PoolHookManager) ProcessOnRemove(ctx context.Context, conn *Conn, reason error) {
|
||||
// Copy slice reference while holding lock (fast)
|
||||
phm.hooksMu.RLock()
|
||||
hooks := phm.hooks
|
||||
phm.hooksMu.RUnlock()
|
||||
|
||||
// Call hooks without holding lock (slow operations)
|
||||
for _, hook := range hooks {
|
||||
hook.OnRemove(ctx, conn, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// GetHookCount returns the number of registered hooks (for testing).
|
||||
func (phm *PoolHookManager) GetHookCount() int {
|
||||
phm.hooksMu.RLock()
|
||||
defer phm.hooksMu.RUnlock()
|
||||
return len(phm.hooks)
|
||||
}
|
||||
|
||||
// GetHooks returns a copy of all registered hooks.
|
||||
func (phm *PoolHookManager) GetHooks() []PoolHook {
|
||||
phm.hooksMu.RLock()
|
||||
defer phm.hooksMu.RUnlock()
|
||||
|
||||
hooks := make([]PoolHook, len(phm.hooks))
|
||||
copy(hooks, phm.hooks)
|
||||
return hooks
|
||||
}
|
||||
|
||||
// Clone creates a copy of the hook manager with the same hooks.
|
||||
// This is used for lock-free atomic updates of the hook manager.
|
||||
func (phm *PoolHookManager) Clone() *PoolHookManager {
|
||||
phm.hooksMu.RLock()
|
||||
defer phm.hooksMu.RUnlock()
|
||||
|
||||
newManager := &PoolHookManager{
|
||||
hooks: make([]PoolHook, len(phm.hooks)),
|
||||
}
|
||||
copy(newManager.hooks, phm.hooks)
|
||||
return newManager
|
||||
}
|
||||
+686
-149
File diff suppressed because it is too large
Load Diff
+50
-4
@@ -1,7 +1,13 @@
|
||||
package pool
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SingleConnPool is a pool that always returns the same connection.
|
||||
// Note: This pool is not thread-safe.
|
||||
// It is intended to be used by clients that need a single connection.
|
||||
type SingleConnPool struct {
|
||||
pool Pooler
|
||||
cn *Conn
|
||||
@@ -10,6 +16,12 @@ type SingleConnPool struct {
|
||||
|
||||
var _ Pooler = (*SingleConnPool)(nil)
|
||||
|
||||
// NewSingleConnPool creates a new single connection pool.
|
||||
// The pool will always return the same connection.
|
||||
// The pool will not:
|
||||
// - Close the connection
|
||||
// - Reconnect the connection
|
||||
// - Track the connection in any way
|
||||
func NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool {
|
||||
return &SingleConnPool{
|
||||
pool: pool,
|
||||
@@ -25,20 +37,47 @@ func (p *SingleConnPool) CloseConn(cn *Conn) error {
|
||||
return p.pool.CloseConn(cn)
|
||||
}
|
||||
|
||||
func (p *SingleConnPool) Get(ctx context.Context) (*Conn, error) {
|
||||
func (p *SingleConnPool) Get(_ context.Context) (*Conn, error) {
|
||||
if p.stickyErr != nil {
|
||||
return nil, p.stickyErr
|
||||
}
|
||||
if p.cn == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
// NOTE: SingleConnPool is NOT thread-safe by design and is used in special scenarios:
|
||||
// - During initialization (connection is in INITIALIZING state)
|
||||
// - During re-authentication (connection is in UNUSABLE state)
|
||||
// - For transactions (connection might be in various states)
|
||||
// We use SetUsed() which forces the transition, rather than TryTransition() which
|
||||
// would fail if the connection is not in IDLE/CREATED state.
|
||||
p.cn.SetUsed(true)
|
||||
p.cn.SetUsedAt(time.Now())
|
||||
return p.cn, nil
|
||||
}
|
||||
|
||||
func (p *SingleConnPool) Put(ctx context.Context, cn *Conn) {}
|
||||
func (p *SingleConnPool) Put(_ context.Context, cn *Conn) {
|
||||
if p.cn == nil {
|
||||
return
|
||||
}
|
||||
if p.cn != cn {
|
||||
return
|
||||
}
|
||||
p.cn.SetUsed(false)
|
||||
}
|
||||
|
||||
func (p *SingleConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
|
||||
func (p *SingleConnPool) Remove(_ context.Context, cn *Conn, reason error) {
|
||||
cn.SetUsed(false)
|
||||
p.cn = nil
|
||||
p.stickyErr = reason
|
||||
}
|
||||
|
||||
// RemoveWithoutTurn has the same behavior as Remove for SingleConnPool
|
||||
// since SingleConnPool doesn't use a turn-based queue system.
|
||||
func (p *SingleConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) {
|
||||
p.Remove(ctx, cn, reason)
|
||||
}
|
||||
|
||||
func (p *SingleConnPool) Close() error {
|
||||
p.cn = nil
|
||||
p.stickyErr = ErrClosed
|
||||
@@ -53,6 +92,13 @@ func (p *SingleConnPool) IdleLen() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Size returns the maximum pool size, which is always 1 for SingleConnPool.
|
||||
func (p *SingleConnPool) Size() int { return 1 }
|
||||
|
||||
func (p *SingleConnPool) Stats() *Stats {
|
||||
return &Stats{}
|
||||
}
|
||||
|
||||
func (p *SingleConnPool) AddPoolHook(_ PoolHook) {}
|
||||
|
||||
func (p *SingleConnPool) RemovePoolHook(_ PoolHook) {}
|
||||
|
||||
+13
@@ -123,6 +123,12 @@ func (p *StickyConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
|
||||
p.ch <- cn
|
||||
}
|
||||
|
||||
// RemoveWithoutTurn has the same behavior as Remove for StickyConnPool
|
||||
// since StickyConnPool doesn't use a turn-based queue system.
|
||||
func (p *StickyConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) {
|
||||
p.Remove(ctx, cn, reason)
|
||||
}
|
||||
|
||||
func (p *StickyConnPool) Close() error {
|
||||
if shared := atomic.AddInt32(&p.shared, -1); shared > 0 {
|
||||
return nil
|
||||
@@ -196,6 +202,13 @@ func (p *StickyConnPool) IdleLen() int {
|
||||
return len(p.ch)
|
||||
}
|
||||
|
||||
// Size returns the maximum pool size, which is always 1 for StickyConnPool.
|
||||
func (p *StickyConnPool) Size() int { return 1 }
|
||||
|
||||
func (p *StickyConnPool) Stats() *Stats {
|
||||
return &Stats{}
|
||||
}
|
||||
|
||||
func (p *StickyConnPool) AddPoolHook(hook PoolHook) {}
|
||||
|
||||
func (p *StickyConnPool) RemovePoolHook(hook PoolHook) {}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type PubSubStats struct {
|
||||
Created uint32
|
||||
Untracked uint32
|
||||
Active uint32
|
||||
}
|
||||
|
||||
// PubSubPool manages a pool of PubSub connections.
|
||||
type PubSubPool struct {
|
||||
opt *Options
|
||||
netDialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Map to track active PubSub connections
|
||||
activeConns sync.Map // map[uint64]*Conn (connID -> conn)
|
||||
closed atomic.Bool
|
||||
stats PubSubStats
|
||||
}
|
||||
|
||||
// NewPubSubPool implements a pool for PubSub connections.
|
||||
// It intentionally does not implement the Pooler interface
|
||||
func NewPubSubPool(opt *Options, netDialer func(ctx context.Context, network, addr string) (net.Conn, error)) *PubSubPool {
|
||||
return &PubSubPool{
|
||||
opt: opt,
|
||||
netDialer: netDialer,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, channels []string) (*Conn, error) {
|
||||
if p.closed.Load() {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
netConn, err := p.netDialer(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cn := NewConnWithBufferSize(netConn, p.opt.ReadBufferSize, p.opt.WriteBufferSize)
|
||||
cn.pubsub = true
|
||||
atomic.AddUint32(&p.stats.Created, 1)
|
||||
return cn, nil
|
||||
|
||||
}
|
||||
|
||||
func (p *PubSubPool) TrackConn(cn *Conn) {
|
||||
atomic.AddUint32(&p.stats.Active, 1)
|
||||
p.activeConns.Store(cn.GetID(), cn)
|
||||
}
|
||||
|
||||
func (p *PubSubPool) UntrackConn(cn *Conn) {
|
||||
atomic.AddUint32(&p.stats.Active, ^uint32(0))
|
||||
atomic.AddUint32(&p.stats.Untracked, 1)
|
||||
p.activeConns.Delete(cn.GetID())
|
||||
}
|
||||
|
||||
func (p *PubSubPool) Close() error {
|
||||
p.closed.Store(true)
|
||||
p.activeConns.Range(func(key, value interface{}) bool {
|
||||
cn := value.(*Conn)
|
||||
_ = cn.Close()
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PubSubPool) Stats() *PubSubStats {
|
||||
// load stats atomically
|
||||
return &PubSubStats{
|
||||
Created: atomic.LoadUint32(&p.stats.Created),
|
||||
Untracked: atomic.LoadUint32(&p.stats.Untracked),
|
||||
Active: atomic.LoadUint32(&p.stats.Active),
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type wantConn struct {
|
||||
mu sync.Mutex // protects ctx, done and sending of the result
|
||||
ctx context.Context // context for dial, cleared after delivered or canceled
|
||||
cancelCtx context.CancelFunc
|
||||
done bool // true after delivered or canceled
|
||||
result chan wantConnResult // channel to deliver connection or error
|
||||
}
|
||||
|
||||
// getCtxForDial returns context for dial or nil if connection was delivered or canceled.
|
||||
func (w *wantConn) getCtxForDial() context.Context {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
return w.ctx
|
||||
}
|
||||
|
||||
func (w *wantConn) tryDeliver(cn *Conn, err error) bool {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.done {
|
||||
return false
|
||||
}
|
||||
|
||||
w.done = true
|
||||
w.ctx = nil
|
||||
|
||||
w.result <- wantConnResult{cn: cn, err: err}
|
||||
close(w.result)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *wantConn) cancel() *Conn {
|
||||
w.mu.Lock()
|
||||
var cn *Conn
|
||||
if w.done {
|
||||
select {
|
||||
case result := <-w.result:
|
||||
cn = result.cn
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
close(w.result)
|
||||
}
|
||||
|
||||
w.done = true
|
||||
w.ctx = nil
|
||||
w.mu.Unlock()
|
||||
|
||||
return cn
|
||||
}
|
||||
|
||||
type wantConnResult struct {
|
||||
cn *Conn
|
||||
err error
|
||||
}
|
||||
|
||||
type wantConnQueue struct {
|
||||
mu sync.RWMutex
|
||||
items []*wantConn
|
||||
}
|
||||
|
||||
func newWantConnQueue() *wantConnQueue {
|
||||
return &wantConnQueue{
|
||||
items: make([]*wantConn, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *wantConnQueue) enqueue(w *wantConn) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.items = append(q.items, w)
|
||||
}
|
||||
|
||||
func (q *wantConnQueue) dequeue() (*wantConn, bool) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
if len(q.items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
item := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return item, true
|
||||
}
|
||||
+99
-3
@@ -12,6 +12,9 @@ import (
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// DefaultBufferSize is the default size for read/write buffers (32 KiB).
|
||||
const DefaultBufferSize = 32 * 1024
|
||||
|
||||
// redis resp protocol data type.
|
||||
const (
|
||||
RespStatus = '+' // +<string>\r\n
|
||||
@@ -47,7 +50,8 @@ func (e RedisError) Error() string { return string(e) }
|
||||
func (RedisError) RedisError() {}
|
||||
|
||||
func ParseErrorReply(line []byte) error {
|
||||
return RedisError(line[1:])
|
||||
msg := string(line[1:])
|
||||
return parseTypedRedisError(msg)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -58,7 +62,13 @@ type Reader struct {
|
||||
|
||||
func NewReader(rd io.Reader) *Reader {
|
||||
return &Reader{
|
||||
rd: bufio.NewReader(rd),
|
||||
rd: bufio.NewReaderSize(rd, DefaultBufferSize),
|
||||
}
|
||||
}
|
||||
|
||||
func NewReaderSize(rd io.Reader, size int) *Reader {
|
||||
return &Reader{
|
||||
rd: bufio.NewReaderSize(rd, size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +100,92 @@ func (r *Reader) PeekReplyType() (byte, error) {
|
||||
return b[0], nil
|
||||
}
|
||||
|
||||
func (r *Reader) PeekPushNotificationName() (string, error) {
|
||||
// "prime" the buffer by peeking at the next byte
|
||||
c, err := r.Peek(1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if c[0] != RespPush {
|
||||
return "", fmt.Errorf("redis: can't peek push notification name, next reply is not a push notification")
|
||||
}
|
||||
|
||||
// peek 36 bytes at most, should be enough to read the push notification name
|
||||
toPeek := 36
|
||||
buffered := r.Buffered()
|
||||
if buffered == 0 {
|
||||
return "", fmt.Errorf("redis: can't peek push notification name, no data available")
|
||||
}
|
||||
if buffered < toPeek {
|
||||
toPeek = buffered
|
||||
}
|
||||
buf, err := r.rd.Peek(toPeek)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if buf[0] != RespPush {
|
||||
return "", fmt.Errorf("redis: can't parse push notification: %q", buf)
|
||||
}
|
||||
|
||||
if len(buf) < 3 {
|
||||
return "", fmt.Errorf("redis: can't parse push notification: %q", buf)
|
||||
}
|
||||
|
||||
// remove push notification type
|
||||
buf = buf[1:]
|
||||
// remove first line - e.g. >2\r\n
|
||||
for i := 0; i < len(buf)-1; i++ {
|
||||
if buf[i] == '\r' && buf[i+1] == '\n' {
|
||||
buf = buf[i+2:]
|
||||
break
|
||||
} else {
|
||||
if buf[i] < '0' || buf[i] > '9' {
|
||||
return "", fmt.Errorf("redis: can't parse push notification: %q", buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(buf) < 2 {
|
||||
return "", fmt.Errorf("redis: can't parse push notification: %q", buf)
|
||||
}
|
||||
// next line should be $<length><string>\r\n or +<length><string>\r\n
|
||||
// should have the type of the push notification name and it's length
|
||||
if buf[0] != RespString && buf[0] != RespStatus {
|
||||
return "", fmt.Errorf("redis: can't parse push notification name: %q", buf)
|
||||
}
|
||||
typeOfName := buf[0]
|
||||
// remove the type of the push notification name
|
||||
buf = buf[1:]
|
||||
if typeOfName == RespString {
|
||||
// remove the length of the string
|
||||
if len(buf) < 2 {
|
||||
return "", fmt.Errorf("redis: can't parse push notification name: %q", buf)
|
||||
}
|
||||
for i := 0; i < len(buf)-1; i++ {
|
||||
if buf[i] == '\r' && buf[i+1] == '\n' {
|
||||
buf = buf[i+2:]
|
||||
break
|
||||
} else {
|
||||
if buf[i] < '0' || buf[i] > '9' {
|
||||
return "", fmt.Errorf("redis: can't parse push notification name: %q", buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(buf) < 2 {
|
||||
return "", fmt.Errorf("redis: can't parse push notification name: %q", buf)
|
||||
}
|
||||
// keep only the notification name
|
||||
for i := 0; i < len(buf)-1; i++ {
|
||||
if buf[i] == '\r' && buf[i+1] == '\n' {
|
||||
buf = buf[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return util.BytesToString(buf), nil
|
||||
}
|
||||
|
||||
// ReadLine Return a valid reply, it will check the protocol or redis error,
|
||||
// and discard the attribute type.
|
||||
func (r *Reader) ReadLine() ([]byte, error) {
|
||||
@@ -106,7 +202,7 @@ func (r *Reader) ReadLine() ([]byte, error) {
|
||||
var blobErr string
|
||||
blobErr, err = r.readStringReply(line)
|
||||
if err == nil {
|
||||
err = RedisError(blobErr)
|
||||
err = parseTypedRedisError(blobErr)
|
||||
}
|
||||
return nil, err
|
||||
case RespAttr:
|
||||
|
||||
+488
@@ -0,0 +1,488 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Typed Redis errors for better error handling with wrapping support.
|
||||
// These errors maintain backward compatibility by keeping the same error messages.
|
||||
|
||||
// LoadingError is returned when Redis is loading the dataset in memory.
|
||||
type LoadingError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *LoadingError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *LoadingError) RedisError() {}
|
||||
|
||||
// NewLoadingError creates a new LoadingError with the given message.
|
||||
func NewLoadingError(msg string) *LoadingError {
|
||||
return &LoadingError{msg: msg}
|
||||
}
|
||||
|
||||
// ReadOnlyError is returned when trying to write to a read-only replica.
|
||||
type ReadOnlyError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *ReadOnlyError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *ReadOnlyError) RedisError() {}
|
||||
|
||||
// NewReadOnlyError creates a new ReadOnlyError with the given message.
|
||||
func NewReadOnlyError(msg string) *ReadOnlyError {
|
||||
return &ReadOnlyError{msg: msg}
|
||||
}
|
||||
|
||||
// MovedError is returned when a key has been moved to a different node in a cluster.
|
||||
type MovedError struct {
|
||||
msg string
|
||||
addr string
|
||||
}
|
||||
|
||||
func (e *MovedError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *MovedError) RedisError() {}
|
||||
|
||||
// Addr returns the address of the node where the key has been moved.
|
||||
func (e *MovedError) Addr() string {
|
||||
return e.addr
|
||||
}
|
||||
|
||||
// NewMovedError creates a new MovedError with the given message and address.
|
||||
func NewMovedError(msg string, addr string) *MovedError {
|
||||
return &MovedError{msg: msg, addr: addr}
|
||||
}
|
||||
|
||||
// AskError is returned when a key is being migrated and the client should ask another node.
|
||||
type AskError struct {
|
||||
msg string
|
||||
addr string
|
||||
}
|
||||
|
||||
func (e *AskError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *AskError) RedisError() {}
|
||||
|
||||
// Addr returns the address of the node to ask.
|
||||
func (e *AskError) Addr() string {
|
||||
return e.addr
|
||||
}
|
||||
|
||||
// NewAskError creates a new AskError with the given message and address.
|
||||
func NewAskError(msg string, addr string) *AskError {
|
||||
return &AskError{msg: msg, addr: addr}
|
||||
}
|
||||
|
||||
// ClusterDownError is returned when the cluster is down.
|
||||
type ClusterDownError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *ClusterDownError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *ClusterDownError) RedisError() {}
|
||||
|
||||
// NewClusterDownError creates a new ClusterDownError with the given message.
|
||||
func NewClusterDownError(msg string) *ClusterDownError {
|
||||
return &ClusterDownError{msg: msg}
|
||||
}
|
||||
|
||||
// TryAgainError is returned when a command cannot be processed and should be retried.
|
||||
type TryAgainError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *TryAgainError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *TryAgainError) RedisError() {}
|
||||
|
||||
// NewTryAgainError creates a new TryAgainError with the given message.
|
||||
func NewTryAgainError(msg string) *TryAgainError {
|
||||
return &TryAgainError{msg: msg}
|
||||
}
|
||||
|
||||
// MasterDownError is returned when the master is down.
|
||||
type MasterDownError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *MasterDownError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *MasterDownError) RedisError() {}
|
||||
|
||||
// NewMasterDownError creates a new MasterDownError with the given message.
|
||||
func NewMasterDownError(msg string) *MasterDownError {
|
||||
return &MasterDownError{msg: msg}
|
||||
}
|
||||
|
||||
// MaxClientsError is returned when the maximum number of clients has been reached.
|
||||
type MaxClientsError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *MaxClientsError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *MaxClientsError) RedisError() {}
|
||||
|
||||
// NewMaxClientsError creates a new MaxClientsError with the given message.
|
||||
func NewMaxClientsError(msg string) *MaxClientsError {
|
||||
return &MaxClientsError{msg: msg}
|
||||
}
|
||||
|
||||
// AuthError is returned when authentication fails.
|
||||
type AuthError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *AuthError) RedisError() {}
|
||||
|
||||
// NewAuthError creates a new AuthError with the given message.
|
||||
func NewAuthError(msg string) *AuthError {
|
||||
return &AuthError{msg: msg}
|
||||
}
|
||||
|
||||
// PermissionError is returned when a user lacks required permissions.
|
||||
type PermissionError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *PermissionError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *PermissionError) RedisError() {}
|
||||
|
||||
// NewPermissionError creates a new PermissionError with the given message.
|
||||
func NewPermissionError(msg string) *PermissionError {
|
||||
return &PermissionError{msg: msg}
|
||||
}
|
||||
|
||||
// ExecAbortError is returned when a transaction is aborted.
|
||||
type ExecAbortError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *ExecAbortError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *ExecAbortError) RedisError() {}
|
||||
|
||||
// NewExecAbortError creates a new ExecAbortError with the given message.
|
||||
func NewExecAbortError(msg string) *ExecAbortError {
|
||||
return &ExecAbortError{msg: msg}
|
||||
}
|
||||
|
||||
// OOMError is returned when Redis is out of memory.
|
||||
type OOMError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *OOMError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *OOMError) RedisError() {}
|
||||
|
||||
// NewOOMError creates a new OOMError with the given message.
|
||||
func NewOOMError(msg string) *OOMError {
|
||||
return &OOMError{msg: msg}
|
||||
}
|
||||
|
||||
// parseTypedRedisError parses a Redis error message and returns a typed error if applicable.
|
||||
// This function maintains backward compatibility by keeping the same error messages.
|
||||
func parseTypedRedisError(msg string) error {
|
||||
// Check for specific error patterns and return typed errors
|
||||
switch {
|
||||
case strings.HasPrefix(msg, "LOADING "):
|
||||
return NewLoadingError(msg)
|
||||
case strings.HasPrefix(msg, "READONLY "):
|
||||
return NewReadOnlyError(msg)
|
||||
case strings.HasPrefix(msg, "MOVED "):
|
||||
// Extract address from "MOVED <slot> <addr>"
|
||||
addr := extractAddr(msg)
|
||||
return NewMovedError(msg, addr)
|
||||
case strings.HasPrefix(msg, "ASK "):
|
||||
// Extract address from "ASK <slot> <addr>"
|
||||
addr := extractAddr(msg)
|
||||
return NewAskError(msg, addr)
|
||||
case strings.HasPrefix(msg, "CLUSTERDOWN "):
|
||||
return NewClusterDownError(msg)
|
||||
case strings.HasPrefix(msg, "TRYAGAIN "):
|
||||
return NewTryAgainError(msg)
|
||||
case strings.HasPrefix(msg, "MASTERDOWN "):
|
||||
return NewMasterDownError(msg)
|
||||
case msg == "ERR max number of clients reached":
|
||||
return NewMaxClientsError(msg)
|
||||
case strings.HasPrefix(msg, "NOAUTH "), strings.HasPrefix(msg, "WRONGPASS "), strings.Contains(msg, "unauthenticated"):
|
||||
return NewAuthError(msg)
|
||||
case strings.HasPrefix(msg, "NOPERM "):
|
||||
return NewPermissionError(msg)
|
||||
case strings.HasPrefix(msg, "EXECABORT "):
|
||||
return NewExecAbortError(msg)
|
||||
case strings.HasPrefix(msg, "OOM "):
|
||||
return NewOOMError(msg)
|
||||
default:
|
||||
// Return generic RedisError for unknown error types
|
||||
return RedisError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// extractAddr extracts the address from MOVED/ASK error messages.
|
||||
// Format: "MOVED <slot> <addr>" or "ASK <slot> <addr>"
|
||||
func extractAddr(msg string) string {
|
||||
ind := strings.LastIndex(msg, " ")
|
||||
if ind == -1 {
|
||||
return ""
|
||||
}
|
||||
return msg[ind+1:]
|
||||
}
|
||||
|
||||
// IsLoadingError checks if an error is a LoadingError, even if wrapped.
|
||||
func IsLoadingError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var loadingErr *LoadingError
|
||||
if errors.As(err, &loadingErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with LOADING prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "LOADING ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "LOADING ")
|
||||
}
|
||||
|
||||
// IsReadOnlyError checks if an error is a ReadOnlyError, even if wrapped.
|
||||
func IsReadOnlyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var readOnlyErr *ReadOnlyError
|
||||
if errors.As(err, &readOnlyErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with READONLY prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "READONLY ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "READONLY ")
|
||||
}
|
||||
|
||||
// IsMovedError checks if an error is a MovedError, even if wrapped.
|
||||
// Returns the error and a boolean indicating if it's a MovedError.
|
||||
func IsMovedError(err error) (*MovedError, bool) {
|
||||
if err == nil {
|
||||
return nil, false
|
||||
}
|
||||
var movedErr *MovedError
|
||||
if errors.As(err, &movedErr) {
|
||||
return movedErr, true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
s := err.Error()
|
||||
if strings.HasPrefix(s, "MOVED ") {
|
||||
// Parse: MOVED 3999 127.0.0.1:6381
|
||||
parts := strings.Split(s, " ")
|
||||
if len(parts) == 3 {
|
||||
return &MovedError{msg: s, addr: parts[2]}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// IsAskError checks if an error is an AskError, even if wrapped.
|
||||
// Returns the error and a boolean indicating if it's an AskError.
|
||||
func IsAskError(err error) (*AskError, bool) {
|
||||
if err == nil {
|
||||
return nil, false
|
||||
}
|
||||
var askErr *AskError
|
||||
if errors.As(err, &askErr) {
|
||||
return askErr, true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
s := err.Error()
|
||||
if strings.HasPrefix(s, "ASK ") {
|
||||
// Parse: ASK 3999 127.0.0.1:6381
|
||||
parts := strings.Split(s, " ")
|
||||
if len(parts) == 3 {
|
||||
return &AskError{msg: s, addr: parts[2]}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// IsClusterDownError checks if an error is a ClusterDownError, even if wrapped.
|
||||
func IsClusterDownError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var clusterDownErr *ClusterDownError
|
||||
if errors.As(err, &clusterDownErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with CLUSTERDOWN prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "CLUSTERDOWN ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "CLUSTERDOWN ")
|
||||
}
|
||||
|
||||
// IsTryAgainError checks if an error is a TryAgainError, even if wrapped.
|
||||
func IsTryAgainError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var tryAgainErr *TryAgainError
|
||||
if errors.As(err, &tryAgainErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with TRYAGAIN prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "TRYAGAIN ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "TRYAGAIN ")
|
||||
}
|
||||
|
||||
// IsMasterDownError checks if an error is a MasterDownError, even if wrapped.
|
||||
func IsMasterDownError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var masterDownErr *MasterDownError
|
||||
if errors.As(err, &masterDownErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with MASTERDOWN prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "MASTERDOWN ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "MASTERDOWN ")
|
||||
}
|
||||
|
||||
// IsMaxClientsError checks if an error is a MaxClientsError, even if wrapped.
|
||||
func IsMaxClientsError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var maxClientsErr *MaxClientsError
|
||||
if errors.As(err, &maxClientsErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with max clients prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "ERR max number of clients reached") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "ERR max number of clients reached")
|
||||
}
|
||||
|
||||
// IsAuthError checks if an error is an AuthError, even if wrapped.
|
||||
func IsAuthError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var authErr *AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with auth error prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) {
|
||||
s := redisErr.Error()
|
||||
return strings.HasPrefix(s, "NOAUTH ") || strings.HasPrefix(s, "WRONGPASS ") || strings.Contains(s, "unauthenticated")
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
s := err.Error()
|
||||
return strings.HasPrefix(s, "NOAUTH ") || strings.HasPrefix(s, "WRONGPASS ") || strings.Contains(s, "unauthenticated")
|
||||
}
|
||||
|
||||
// IsPermissionError checks if an error is a PermissionError, even if wrapped.
|
||||
func IsPermissionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var permErr *PermissionError
|
||||
if errors.As(err, &permErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with NOPERM prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "NOPERM ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "NOPERM ")
|
||||
}
|
||||
|
||||
// IsExecAbortError checks if an error is an ExecAbortError, even if wrapped.
|
||||
func IsExecAbortError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var execAbortErr *ExecAbortError
|
||||
if errors.As(err, &execAbortErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with EXECABORT prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "EXECABORT ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "EXECABORT ")
|
||||
}
|
||||
|
||||
// IsOOMError checks if an error is an OOMError, even if wrapped.
|
||||
func IsOOMError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var oomErr *OOMError
|
||||
if errors.As(err, &oomErr) {
|
||||
return true
|
||||
}
|
||||
// Check if wrapped error is a RedisError with OOM prefix
|
||||
var redisErr RedisError
|
||||
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "OOM ") {
|
||||
return true
|
||||
}
|
||||
// Fallback to string checking for backward compatibility
|
||||
return strings.HasPrefix(err.Error(), "OOM ")
|
||||
}
|
||||
+53
@@ -66,56 +66,95 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||
case string:
|
||||
return w.string(v)
|
||||
case *string:
|
||||
if v == nil {
|
||||
return w.string("")
|
||||
}
|
||||
return w.string(*v)
|
||||
case []byte:
|
||||
return w.bytes(v)
|
||||
case int:
|
||||
return w.int(int64(v))
|
||||
case *int:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
return w.int(int64(*v))
|
||||
case int8:
|
||||
return w.int(int64(v))
|
||||
case *int8:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
return w.int(int64(*v))
|
||||
case int16:
|
||||
return w.int(int64(v))
|
||||
case *int16:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
return w.int(int64(*v))
|
||||
case int32:
|
||||
return w.int(int64(v))
|
||||
case *int32:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
return w.int(int64(*v))
|
||||
case int64:
|
||||
return w.int(v)
|
||||
case *int64:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
return w.int(*v)
|
||||
case uint:
|
||||
return w.uint(uint64(v))
|
||||
case *uint:
|
||||
if v == nil {
|
||||
return w.uint(0)
|
||||
}
|
||||
return w.uint(uint64(*v))
|
||||
case uint8:
|
||||
return w.uint(uint64(v))
|
||||
case *uint8:
|
||||
if v == nil {
|
||||
return w.string("")
|
||||
}
|
||||
return w.uint(uint64(*v))
|
||||
case uint16:
|
||||
return w.uint(uint64(v))
|
||||
case *uint16:
|
||||
if v == nil {
|
||||
return w.uint(0)
|
||||
}
|
||||
return w.uint(uint64(*v))
|
||||
case uint32:
|
||||
return w.uint(uint64(v))
|
||||
case *uint32:
|
||||
if v == nil {
|
||||
return w.uint(0)
|
||||
}
|
||||
return w.uint(uint64(*v))
|
||||
case uint64:
|
||||
return w.uint(v)
|
||||
case *uint64:
|
||||
if v == nil {
|
||||
return w.uint(0)
|
||||
}
|
||||
return w.uint(*v)
|
||||
case float32:
|
||||
return w.float(float64(v))
|
||||
case *float32:
|
||||
if v == nil {
|
||||
return w.float(0)
|
||||
}
|
||||
return w.float(float64(*v))
|
||||
case float64:
|
||||
return w.float(v)
|
||||
case *float64:
|
||||
if v == nil {
|
||||
return w.float(0)
|
||||
}
|
||||
return w.float(*v)
|
||||
case bool:
|
||||
if v {
|
||||
@@ -123,6 +162,9 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||
}
|
||||
return w.int(0)
|
||||
case *bool:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
if *v {
|
||||
return w.int(1)
|
||||
}
|
||||
@@ -130,8 +172,19 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||
case time.Time:
|
||||
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
|
||||
return w.bytes(w.numBuf)
|
||||
case *time.Time:
|
||||
if v == nil {
|
||||
v = &time.Time{}
|
||||
}
|
||||
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
|
||||
return w.bytes(w.numBuf)
|
||||
case time.Duration:
|
||||
return w.int(v.Nanoseconds())
|
||||
case *time.Duration:
|
||||
if v == nil {
|
||||
return w.int(0)
|
||||
}
|
||||
return w.int(v.Nanoseconds())
|
||||
case encoding.BinaryMarshaler:
|
||||
b, err := v.MarshalBinary()
|
||||
if err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user