1
0
mirror of https://github.com/redis/go-redis.git synced 2025-06-05 06:42:39 +03:00

Merge branch 'master' into ndyakov/token-based-auth

This commit is contained in:
Nedyalko Dyakov 2025-05-12 19:08:57 +03:00 committed by GitHub
commit 8f05aef18e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 407 additions and 48 deletions

View File

@ -25,7 +25,7 @@ runs:
# Mapping of redis version to redis testing containers
declare -A redis_version_mapping=(
["8.0-RC2"]="8.0-RC2-pre"
["8.0.1"]="8.0.1-pre"
["7.4.2"]="rs-7.4.0-v2"
["7.2.7"]="rs-7.2.0-v14"
)

View File

@ -2,9 +2,9 @@ name: Go
on:
push:
branches: [master, v9, v9.7]
branches: [master, v9, v9.7, v9.8]
pull_request:
branches: [master, v9, v9.7]
branches: [master, v9, v9.7, v9.8]
permissions:
contents: read
@ -18,7 +18,7 @@ jobs:
fail-fast: false
matrix:
redis-version:
- "8.0-RC2" # 8.0 RC2
- "8.0.1" # 8.0.1
- "7.4.2" # should use redis stack 7.4
go-version:
- "1.23.x"
@ -43,7 +43,7 @@ jobs:
# Mapping of redis version to redis testing containers
declare -A redis_version_mapping=(
["8.0-RC2"]="8.0-RC2-pre"
["8.0.1"]="8.0.1-pre"
["7.4.2"]="rs-7.4.0-v2"
)
if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then
@ -72,7 +72,7 @@ jobs:
fail-fast: false
matrix:
redis-version:
- "8.0-RC2" # 8.0 RC2
- "8.0.1" # 8.0.1
- "7.4.2" # should use redis stack 7.4
- "7.2.7" # should redis stack 7.2
go-version:

View File

@ -13,10 +13,9 @@ name: "CodeQL"
on:
push:
branches: [ master ]
branches: [master, v9, v9.7, v9.8]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [master, v9, v9.7, v9.8]
jobs:
analyze:

View File

@ -8,6 +8,7 @@ on:
- master
- main
- v9
- v9.8
pull_request:
permissions:
@ -21,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v7.0.0
uses: golangci/golangci-lint-action@v8.0.0
with:
verify: true

View File

@ -2,7 +2,7 @@ name: RE Tests
on:
push:
branches: [master]
branches: [master, v9, v9.7, v9.8]
pull_request:
permissions:

80
RELEASE-NOTES.md Normal file
View File

@ -0,0 +1,80 @@
# Release Notes
# 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)

View File

@ -5,7 +5,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0
go.uber.org/zap v1.24.0
)

View File

@ -4,7 +4,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.8.0-beta.1
require github.com/redis/go-redis/v9 v9.8.0
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/davecgh/go-spew v1.1.1
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0
)
require (

View File

@ -4,7 +4,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.8.0-beta.1
require github.com/redis/go-redis/v9 v9.8.0
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd
require (
github.com/redis/go-redis/extra/redisotel/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/extra/redisotel/v9 v9.8.0
github.com/redis/go-redis/v9 v9.8.0
github.com/uptrace/uptrace-go v1.21.0
go.opentelemetry.io/otel v1.22.0
)
@ -25,7 +25,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect

View File

@ -4,7 +4,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.8.0-beta.1
require github.com/redis/go-redis/v9 v9.8.0
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/davecgh/go-spew v1.1.1
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0
)
require (

View File

@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0
github.com/redis/go-redis/v9 v9.8.0
go.opencensus.io v0.24.0
)

View File

@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/bsm/ginkgo/v2 v2.12.0
github.com/bsm/gomega v1.27.10
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0
)
require (

View File

@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0
github.com/redis/go-redis/v9 v9.8.0
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/metric v1.22.0
go.opentelemetry.io/otel/sdk v1.22.0

View File

@ -127,6 +127,22 @@ func reportPoolStats(rdb *redis.Client, conf *config) error {
return err
}
hits, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.hits",
metric.WithDescription("The number of times free connection was found in the pool"),
)
if err != nil {
return err
}
misses, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.misses",
metric.WithDescription("The number of times free connection was not found in the pool"),
)
if err != nil {
return err
}
redisConf := rdb.Options()
_, err = conf.meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {
@ -140,6 +156,8 @@ func reportPoolStats(rdb *redis.Client, conf *config) error {
o.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributes(usedAttrs...))
o.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributes(labels...))
o.ObserveInt64(hits, int64(stats.Hits), metric.WithAttributes(labels...))
o.ObserveInt64(misses, int64(stats.Misses), metric.WithAttributes(labels...))
return nil
},
idleMax,
@ -147,6 +165,8 @@ func reportPoolStats(rdb *redis.Client, conf *config) error {
connsMax,
usage,
timeouts,
hits,
misses,
)
return err

View File

@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/prometheus/client_golang v1.14.0
github.com/redis/go-redis/v9 v9.8.0-beta.1
github.com/redis/go-redis/v9 v9.8.0
)
require (

11
helper/helper.go Normal file
View File

@ -0,0 +1,11 @@
package helper
import "github.com/redis/go-redis/v9/internal/util"
func ParseFloat(s string) (float64, error) {
return util.ParseStringToFloat(s)
}
func MustParseFloat(s string) float64 {
return util.MustParseFloat(s)
}

View File

@ -33,9 +33,11 @@ var timers = sync.Pool{
// Stats contains pool state information and accumulated stats.
type Stats struct {
Hits uint32 // number of times free connection was found in the pool
Misses uint32 // number of times free connection was NOT found in the pool
Timeouts uint32 // number of times a wait timeout occurred
Hits uint32 // number of times free connection was found in the pool
Misses uint32 // number of times free connection was NOT found in the pool
Timeouts uint32 // number of times a wait timeout occurred
WaitCount uint32 // number of times a connection was waited
WaitDurationNs int64 // total time spent for waiting a connection in nanoseconds
TotalConns uint32 // number of total connections in the pool
IdleConns uint32 // number of idle connections in the pool
@ -90,7 +92,8 @@ type ConnPool struct {
poolSize int
idleConnsLen int
stats Stats
stats Stats
waitDurationNs atomic.Int64
_closed uint32 // atomic
}
@ -320,6 +323,7 @@ func (p *ConnPool) waitTurn(ctx context.Context) error {
default:
}
start := time.Now()
timer := timers.Get().(*time.Timer)
timer.Reset(p.cfg.PoolTimeout)
@ -331,6 +335,8 @@ func (p *ConnPool) waitTurn(ctx context.Context) error {
timers.Put(timer)
return ctx.Err()
case p.queue <- struct{}{}:
p.waitDurationNs.Add(time.Since(start).Nanoseconds())
atomic.AddUint32(&p.stats.WaitCount, 1)
if !timer.Stop() {
<-timer.C
}
@ -457,9 +463,11 @@ func (p *ConnPool) IdleLen() int {
func (p *ConnPool) Stats() *Stats {
return &Stats{
Hits: atomic.LoadUint32(&p.stats.Hits),
Misses: atomic.LoadUint32(&p.stats.Misses),
Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
Hits: atomic.LoadUint32(&p.stats.Hits),
Misses: atomic.LoadUint32(&p.stats.Misses),
Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
WaitCount: atomic.LoadUint32(&p.stats.WaitCount),
WaitDurationNs: p.waitDurationNs.Load(),
TotalConns: uint32(p.Len()),
IdleConns: uint32(p.IdleLen()),

View File

@ -59,12 +59,14 @@ var _ = Describe("ConnPool", func() {
time.Sleep(time.Second)
Expect(connPool.Stats()).To(Equal(&pool.Stats{
Hits: 0,
Misses: 0,
Timeouts: 0,
TotalConns: 0,
IdleConns: 0,
StaleConns: 0,
Hits: 0,
Misses: 0,
Timeouts: 0,
WaitCount: 0,
WaitDurationNs: 0,
TotalConns: 0,
IdleConns: 0,
StaleConns: 0,
}))
})
@ -358,4 +360,31 @@ var _ = Describe("race", func() {
Expect(stats.IdleConns).To(Equal(uint32(0)))
Expect(stats.TotalConns).To(Equal(uint32(opt.PoolSize)))
})
It("wait", func() {
opt := &pool.Options{
Dialer: func(ctx context.Context) (net.Conn, error) {
return &net.TCPConn{}, nil
},
PoolSize: 1,
PoolTimeout: 3 * time.Second,
}
p := pool.NewConnPool(opt)
wait := make(chan struct{})
conn, _ := p.Get(ctx)
go func() {
_, _ = p.Get(ctx)
wait <- struct{}{}
}()
time.Sleep(time.Second)
p.Put(ctx, conn)
<-wait
stats := p.Stats()
Expect(stats.IdleConns).To(Equal(uint32(0)))
Expect(stats.TotalConns).To(Equal(uint32(1)))
Expect(stats.WaitCount).To(Equal(uint32(1)))
Expect(stats.WaitDurationNs).To(BeNumerically("~", time.Second.Nanoseconds(), 100*time.Millisecond.Nanoseconds()))
})
})

30
internal/util/convert.go Normal file
View File

@ -0,0 +1,30 @@
package util
import (
"fmt"
"math"
"strconv"
)
// ParseFloat parses a Redis RESP3 float reply into a Go float64,
// handling "inf", "-inf", "nan" per Redis conventions.
func ParseStringToFloat(s string) (float64, error) {
switch s {
case "inf":
return math.Inf(1), nil
case "-inf":
return math.Inf(-1), nil
case "nan", "-nan":
return math.NaN(), nil
}
return strconv.ParseFloat(s, 64)
}
// MustParseFloat is like ParseFloat but panics on parse errors.
func MustParseFloat(s string) float64 {
f, err := ParseStringToFloat(s)
if err != nil {
panic(fmt.Sprintf("redis: failed to parse float %q: %v", s, err))
}
return f
}

View File

@ -0,0 +1,40 @@
package util
import (
"math"
"testing"
)
func TestParseStringToFloat(t *testing.T) {
tests := []struct {
in string
want float64
ok bool
}{
{"1.23", 1.23, true},
{"inf", math.Inf(1), true},
{"-inf", math.Inf(-1), true},
{"nan", math.NaN(), true},
{"oops", 0, false},
}
for _, tc := range tests {
got, err := ParseStringToFloat(tc.in)
if tc.ok {
if err != nil {
t.Fatalf("ParseFloat(%q) error: %v", tc.in, err)
}
if math.IsNaN(tc.want) {
if !math.IsNaN(got) {
t.Errorf("ParseFloat(%q) = %v; want NaN", tc.in, got)
}
} else if got != tc.want {
t.Errorf("ParseFloat(%q) = %v; want %v", tc.in, got, tc.want)
}
} else {
if err == nil {
t.Errorf("ParseFloat(%q) expected error, got nil", tc.in)
}
}
}
}

View File

@ -100,7 +100,8 @@ var _ = BeforeSuite(func() {
fmt.Printf("RECluster: %v\n", RECluster)
fmt.Printf("RCEDocker: %v\n", RCEDocker)
fmt.Printf("REDIS_VERSION: %v\n", RedisVersion)
fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion)
fmt.Printf("CLIENT_LIBS_TEST_IMAGE: %v\n", os.Getenv("CLIENT_LIBS_TEST_IMAGE"))
if RedisVersion < 7.0 || RedisVersion > 9 {
panic("incorrect or not supported redis version")

View File

@ -927,6 +927,9 @@ type ClusterClient struct {
// NewClusterClient returns a Redis Cluster client as described in
// http://redis.io/topics/cluster-spec.
func NewClusterClient(opt *ClusterOptions) *ClusterClient {
if opt == nil {
panic("redis: NewClusterClient nil options")
}
opt.init()
c := &ClusterClient{

View File

@ -736,6 +736,9 @@ type Client struct {
// NewClient returns a client to the Redis Server specified by Options.
func NewClient(opt *Options) *Client {
if opt == nil {
panic("redis: NewClient nil options")
}
opt.init()
c := Client{

View File

@ -896,3 +896,55 @@ func (m *mockStreamingProvider) Subscribe(listener auth.CredentialsListener) (au
return
}, nil
}
var _ = Describe("Client creation", func() {
Context("simple client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewClient(nil)
}).To(Panic())
})
})
Context("cluster client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewClusterClient(nil)
}).To(Panic())
})
})
Context("ring client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewRing(nil)
}).To(Panic())
})
})
Context("universal client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewUniversalClient(nil)
}).To(Panic())
})
})
Context("failover client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewFailoverClient(nil)
}).To(Panic())
})
})
Context("failover cluster client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewFailoverClusterClient(nil)
}).To(Panic())
})
})
Context("sentinel client with nil options", func() {
It("panics", func() {
Expect(func() {
redis.NewSentinelClient(nil)
}).To(Panic())
})
})
})

View File

@ -523,6 +523,9 @@ type Ring struct {
}
func NewRing(opt *RingOptions) *Ring {
if opt == nil {
panic("redis: NewRing nil options")
}
opt.init()
hbCtx, hbCancel := context.WithCancel(context.Background())

View File

@ -1,15 +1,18 @@
package redis_test
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"strconv"
"math"
"strings"
"time"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/helper"
)
func WaitForIndexing(c *redis.Client, index string) {
@ -27,6 +30,14 @@ func WaitForIndexing(c *redis.Client, index string) {
}
}
func encodeFloat32Vector(vec []float32) []byte {
buf := new(bytes.Buffer)
for _, v := range vec {
binary.Write(buf, binary.LittleEndian, v)
}
return buf.Bytes()
}
var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
ctx := context.TODO()
var client *redis.Client
@ -693,9 +704,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
Expect(err).NotTo(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Rows)).To(BeEquivalentTo(2))
score1, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]), 64)
score1, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64)
score2, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
Expect(score1).To(BeNumerically(">", score2))
@ -712,9 +723,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
Expect(err).NotTo(HaveOccurred())
Expect(resDM).ToNot(BeNil())
Expect(len(resDM.Rows)).To(BeEquivalentTo(2))
score1DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]), 64)
score1DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64)
score2DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
Expect(score1DM).To(BeNumerically(">", score2DM))
@ -1684,6 +1695,56 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1"))
})
It("should return special float scores in FT.SEARCH vecsim", Label("search", "ftsearch", "vecsim"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
vecField := &redis.FTFlatOptions{
Type: "FLOAT32",
Dim: 2,
DistanceMetric: "IP",
}
_, err := client.FTCreate(ctx, "idx_vec",
&redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"doc:"}},
&redis.FieldSchema{FieldName: "vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: vecField}}).Result()
Expect(err).NotTo(HaveOccurred())
WaitForIndexing(client, "idx_vec")
bigPos := []float32{1e38, 1e38}
bigNeg := []float32{-1e38, -1e38}
nanVec := []float32{float32(math.NaN()), 0}
negNanVec := []float32{float32(math.Copysign(math.NaN(), -1)), 0}
client.HSet(ctx, "doc:1", "vector", encodeFloat32Vector(bigPos))
client.HSet(ctx, "doc:2", "vector", encodeFloat32Vector(bigNeg))
client.HSet(ctx, "doc:3", "vector", encodeFloat32Vector(nanVec))
client.HSet(ctx, "doc:4", "vector", encodeFloat32Vector(negNanVec))
searchOptions := &redis.FTSearchOptions{WithScores: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(bigPos)}}
res, err := client.FTSearchWithArgs(ctx, "idx_vec", "*=>[KNN 4 @vector $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(4))
var scores []float64
for _, row := range res.Docs {
raw := fmt.Sprintf("%v", row.Fields["__vector_score"])
f, err := helper.ParseFloat(raw)
Expect(err).NotTo(HaveOccurred())
scores = append(scores, f)
}
Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(1))))
Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(-1))))
// For NaN values, use a custom check since NaN != NaN in floating point math
nanCount := 0
for _, score := range scores {
if math.IsNaN(score) {
nanCount++
}
}
Expect(nanCount).To(Equal(2))
})
It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() {
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{
@ -1871,17 +1932,20 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(client, "aggTimeoutHeavy")
const totalDocs = 10000
const totalDocs = 100000
for i := 0; i < totalDocs; i++ {
key := fmt.Sprintf("doc%d", i)
_, err := client.HSet(ctx, key, "n", i).Result()
Expect(err).NotTo(HaveOccurred())
}
// default behaviour was changed in 8.0.1, set to fail to validate the timeout was triggered
err = client.ConfigSet(ctx, "search-on-timeout", "fail").Err()
Expect(err).NotTo(HaveOccurred())
options := &redis.FTAggregateOptions{
SortBy: []redis.FTAggregateSortBy{{FieldName: "@n", Desc: true}},
LimitOffset: 0,
Limit: 100,
Limit: 100000,
Timeout: 1, // 1 ms timeout, expected to trigger a timeout error.
}
_, err = client.FTAggregateWithArgs(ctx, "aggTimeoutHeavy", "*", options).Result()

View File

@ -224,6 +224,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
// for automatic failover. It's safe for concurrent use by multiple
// goroutines.
func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
if failoverOpt == nil {
panic("redis: NewFailoverClient nil options")
}
if failoverOpt.RouteByLatency {
panic("to route commands by latency, use NewFailoverClusterClient")
}
@ -312,6 +316,9 @@ type SentinelClient struct {
}
func NewSentinelClient(opt *Options) *SentinelClient {
if opt == nil {
panic("redis: NewSentinelClient nil options")
}
opt.init()
c := &SentinelClient{
baseClient: &baseClient{
@ -827,6 +834,10 @@ func contains(slice []string, str string) bool {
// NewFailoverClusterClient returns a client that supports routing read-only commands
// to a replica node.
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
if failoverOpt == nil {
panic("redis: NewFailoverClusterClient nil options")
}
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
copy(sentinelAddrs, failoverOpt.SentinelAddrs)

View File

@ -267,6 +267,10 @@ var (
// a ClusterClient is returned.
// 4. Otherwise, a single-node Client is returned.
func NewUniversalClient(opts *UniversalOptions) UniversalClient {
if opts == nil {
panic("redis: NewUniversalClient nil options")
}
switch {
case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode):
return NewFailoverClusterClient(opts.Failover())

View File

@ -2,5 +2,5 @@ package redis
// Version is the current release version.
func Version() string {
return "9.8.0-beta.1"
return "9.8.0"
}