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:
commit
8f05aef18e
2
.github/actions/run-tests/action.yml
vendored
2
.github/actions/run-tests/action.yml
vendored
@ -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"
|
||||
)
|
||||
|
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -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:
|
||||
|
5
.github/workflows/codeql-analysis.yml
vendored
5
.github/workflows/codeql-analysis.yml
vendored
@ -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:
|
||||
|
3
.github/workflows/golangci-lint.yml
vendored
3
.github/workflows/golangci-lint.yml
vendored
@ -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
|
||||
|
||||
|
2
.github/workflows/test-redis-enterprise.yml
vendored
2
.github/workflows/test-redis-enterprise.yml
vendored
@ -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
80
RELEASE-NOTES.md
Normal 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)
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
11
helper/helper.go
Normal 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)
|
||||
}
|
@ -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()),
|
||||
|
@ -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
30
internal/util/convert.go
Normal 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
|
||||
}
|
40
internal/util/convert_test.go
Normal file
40
internal/util/convert_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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{
|
||||
|
3
redis.go
3
redis.go
@ -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{
|
||||
|
@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
3
ring.go
3
ring.go
@ -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())
|
||||
|
@ -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()
|
||||
|
11
sentinel.go
11
sentinel.go
@ -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)
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user