Skip to content

Commit 68349f4

Browse files
committed
Merge branch 'master' of https://github.com/redis/go-redis into xinfo-groups-nil-lag-support
2 parents 40eb89c + 03c2c0b commit 68349f4

19 files changed

+765
-53
lines changed

.github/actions/run-tests/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ runs:
2525
2626
# Mapping of redis version to redis testing containers
2727
declare -A redis_version_mapping=(
28-
["8.0-RC2"]="8.0-RC2-pre"
28+
["8.0.1"]="8.0.1-pre"
2929
["7.4.2"]="rs-7.4.0-v2"
3030
["7.2.7"]="rs-7.2.0-v14"
3131
)

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
redis-version:
21-
- "8.0-RC2" # 8.0 RC2
21+
- "8.0.1" # 8.0.1
2222
- "7.4.2" # should use redis stack 7.4
2323
go-version:
2424
- "1.23.x"
@@ -43,7 +43,7 @@ jobs:
4343
4444
# Mapping of redis version to redis testing containers
4545
declare -A redis_version_mapping=(
46-
["8.0-RC2"]="8.0-RC2-pre"
46+
["8.0.1"]="8.0.1-pre"
4747
["7.4.2"]="rs-7.4.0-v2"
4848
)
4949
if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then
@@ -72,7 +72,7 @@ jobs:
7272
fail-fast: false
7373
matrix:
7474
redis-version:
75-
- "8.0-RC2" # 8.0 RC2
75+
- "8.0.1" # 8.0.1
7676
- "7.4.2" # should use redis stack 7.4
7777
- "7.2.7" # should redis stack 7.2
7878
go-version:

error.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import (
1515
// ErrClosed performs any operation on the closed client will return this error.
1616
var ErrClosed = pool.ErrClosed
1717

18+
// ErrPoolExhausted is returned from a pool connection method
19+
// when the maximum number of database connections in the pool has been reached.
20+
var ErrPoolExhausted = pool.ErrPoolExhausted
21+
22+
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
23+
var ErrPoolTimeout = pool.ErrPoolTimeout
24+
1825
// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.
1926
func HasErrorPrefix(err error, prefix string) bool {
2027
var rErr Error

example_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func ExampleMapStringStringCmd_Scan() {
359359
// Get the map. The same approach works for HmGet().
360360
res := rdb.HGetAll(ctx, "map")
361361
if res.Err() != nil {
362-
panic(err)
362+
panic(res.Err())
363363
}
364364

365365
type data struct {
@@ -392,7 +392,7 @@ func ExampleSliceCmd_Scan() {
392392

393393
res := rdb.MGet(ctx, "name", "count", "empty", "correct")
394394
if res.Err() != nil {
395-
panic(err)
395+
panic(res.Err())
396396
}
397397

398398
type data struct {

export_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
"github.com/redis/go-redis/v9/internal/pool"
1212
)
1313

14-
var ErrPoolTimeout = pool.ErrPoolTimeout
15-
1614
func (c *baseClient) Pool() pool.Pooler {
1715
return c.connPool
1816
}

helper/helper.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package helper
2+
3+
import "github.com/redis/go-redis/v9/internal/util"
4+
5+
func ParseFloat(s string) (float64, error) {
6+
return util.ParseStringToFloat(s)
7+
}
8+
9+
func MustParseFloat(s string) float64 {
10+
return util.MustParseFloat(s)
11+
}

internal/pool/pool_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,33 @@ var _ = Describe("race", func() {
387387
Expect(stats.WaitCount).To(Equal(uint32(1)))
388388
Expect(stats.WaitDurationNs).To(BeNumerically("~", time.Second.Nanoseconds(), 100*time.Millisecond.Nanoseconds()))
389389
})
390+
391+
It("timeout", func() {
392+
testPoolTimeout := 1 * time.Second
393+
opt := &pool.Options{
394+
Dialer: func(ctx context.Context) (net.Conn, error) {
395+
// Artificial delay to force pool timeout
396+
time.Sleep(3 * testPoolTimeout)
397+
398+
return &net.TCPConn{}, nil
399+
},
400+
PoolSize: 1,
401+
PoolTimeout: testPoolTimeout,
402+
}
403+
p := pool.NewConnPool(opt)
404+
405+
stats := p.Stats()
406+
Expect(stats.Timeouts).To(Equal(uint32(0)))
407+
408+
conn, err := p.Get(ctx)
409+
Expect(err).NotTo(HaveOccurred())
410+
_, err = p.Get(ctx)
411+
Expect(err).To(MatchError(pool.ErrPoolTimeout))
412+
p.Put(ctx, conn)
413+
conn, err = p.Get(ctx)
414+
Expect(err).NotTo(HaveOccurred())
415+
416+
stats = p.Stats()
417+
Expect(stats.Timeouts).To(Equal(uint32(1)))
418+
})
390419
})

internal/util.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,7 @@ func isLower(s string) bool {
4949
}
5050

5151
func ReplaceSpaces(s string) string {
52-
// Pre-allocate a builder with the same length as s to minimize allocations.
53-
// This is a basic optimization; adjust the initial size based on your use case.
54-
var builder strings.Builder
55-
builder.Grow(len(s))
56-
57-
for _, char := range s {
58-
if char == ' ' {
59-
// Replace space with a hyphen.
60-
builder.WriteRune('-')
61-
} else {
62-
// Copy the character as-is.
63-
builder.WriteRune(char)
64-
}
65-
}
66-
67-
return builder.String()
52+
return strings.ReplaceAll(s, " ", "-")
6853
}
6954

7055
func GetAddr(addr string) string {

internal/util/convert.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package util
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"strconv"
7+
)
8+
9+
// ParseFloat parses a Redis RESP3 float reply into a Go float64,
10+
// handling "inf", "-inf", "nan" per Redis conventions.
11+
func ParseStringToFloat(s string) (float64, error) {
12+
switch s {
13+
case "inf":
14+
return math.Inf(1), nil
15+
case "-inf":
16+
return math.Inf(-1), nil
17+
case "nan", "-nan":
18+
return math.NaN(), nil
19+
}
20+
return strconv.ParseFloat(s, 64)
21+
}
22+
23+
// MustParseFloat is like ParseFloat but panics on parse errors.
24+
func MustParseFloat(s string) float64 {
25+
f, err := ParseStringToFloat(s)
26+
if err != nil {
27+
panic(fmt.Sprintf("redis: failed to parse float %q: %v", s, err))
28+
}
29+
return f
30+
}

internal/util/convert_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package util
2+
3+
import (
4+
"math"
5+
"testing"
6+
)
7+
8+
func TestParseStringToFloat(t *testing.T) {
9+
tests := []struct {
10+
in string
11+
want float64
12+
ok bool
13+
}{
14+
{"1.23", 1.23, true},
15+
{"inf", math.Inf(1), true},
16+
{"-inf", math.Inf(-1), true},
17+
{"nan", math.NaN(), true},
18+
{"oops", 0, false},
19+
}
20+
21+
for _, tc := range tests {
22+
got, err := ParseStringToFloat(tc.in)
23+
if tc.ok {
24+
if err != nil {
25+
t.Fatalf("ParseFloat(%q) error: %v", tc.in, err)
26+
}
27+
if math.IsNaN(tc.want) {
28+
if !math.IsNaN(got) {
29+
t.Errorf("ParseFloat(%q) = %v; want NaN", tc.in, got)
30+
}
31+
} else if got != tc.want {
32+
t.Errorf("ParseFloat(%q) = %v; want %v", tc.in, got, tc.want)
33+
}
34+
} else {
35+
if err == nil {
36+
t.Errorf("ParseFloat(%q) expected error, got nil", tc.in)
37+
}
38+
}
39+
}
40+
}

internal/util/strconv_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package util
2+
3+
import (
4+
"math"
5+
"testing"
6+
)
7+
8+
func TestAtoi(t *testing.T) {
9+
tests := []struct {
10+
input []byte
11+
expected int
12+
wantErr bool
13+
}{
14+
{[]byte("123"), 123, false},
15+
{[]byte("-456"), -456, false},
16+
{[]byte("abc"), 0, true},
17+
}
18+
19+
for _, tt := range tests {
20+
result, err := Atoi(tt.input)
21+
if (err != nil) != tt.wantErr {
22+
t.Errorf("Atoi(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
23+
}
24+
if result != tt.expected && !tt.wantErr {
25+
t.Errorf("Atoi(%q) = %d, want %d", tt.input, result, tt.expected)
26+
}
27+
}
28+
}
29+
30+
func TestParseInt(t *testing.T) {
31+
tests := []struct {
32+
input []byte
33+
base int
34+
bitSize int
35+
expected int64
36+
wantErr bool
37+
}{
38+
{[]byte("123"), 10, 64, 123, false},
39+
{[]byte("-7F"), 16, 64, -127, false},
40+
{[]byte("zzz"), 36, 64, 46655, false},
41+
{[]byte("invalid"), 10, 64, 0, true},
42+
}
43+
44+
for _, tt := range tests {
45+
result, err := ParseInt(tt.input, tt.base, tt.bitSize)
46+
if (err != nil) != tt.wantErr {
47+
t.Errorf("ParseInt(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr)
48+
}
49+
if result != tt.expected && !tt.wantErr {
50+
t.Errorf("ParseInt(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected)
51+
}
52+
}
53+
}
54+
55+
func TestParseUint(t *testing.T) {
56+
tests := []struct {
57+
input []byte
58+
base int
59+
bitSize int
60+
expected uint64
61+
wantErr bool
62+
}{
63+
{[]byte("255"), 10, 8, 255, false},
64+
{[]byte("FF"), 16, 16, 255, false},
65+
{[]byte("-1"), 10, 8, 0, true}, // negative should error for unsigned
66+
}
67+
68+
for _, tt := range tests {
69+
result, err := ParseUint(tt.input, tt.base, tt.bitSize)
70+
if (err != nil) != tt.wantErr {
71+
t.Errorf("ParseUint(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr)
72+
}
73+
if result != tt.expected && !tt.wantErr {
74+
t.Errorf("ParseUint(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected)
75+
}
76+
}
77+
}
78+
79+
func TestParseFloat(t *testing.T) {
80+
tests := []struct {
81+
input []byte
82+
bitSize int
83+
expected float64
84+
wantErr bool
85+
}{
86+
{[]byte("3.14"), 64, 3.14, false},
87+
{[]byte("-2.71"), 64, -2.71, false},
88+
{[]byte("NaN"), 64, math.NaN(), false},
89+
{[]byte("invalid"), 64, 0, true},
90+
}
91+
92+
for _, tt := range tests {
93+
result, err := ParseFloat(tt.input, tt.bitSize)
94+
if (err != nil) != tt.wantErr {
95+
t.Errorf("ParseFloat(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
96+
}
97+
if !tt.wantErr && !(math.IsNaN(tt.expected) && math.IsNaN(result)) && result != tt.expected {
98+
t.Errorf("ParseFloat(%q) = %v, want %v", tt.input, result, tt.expected)
99+
}
100+
}
101+
}

internal/util_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package internal
22

33
import (
4+
"runtime"
45
"strings"
56
"testing"
67

@@ -72,3 +73,36 @@ func TestGetAddr(t *testing.T) {
7273
Expect(GetAddr("127")).To(Equal(""))
7374
})
7475
}
76+
77+
func BenchmarkReplaceSpaces(b *testing.B) {
78+
version := runtime.Version()
79+
for i := 0; i < b.N; i++ {
80+
_ = ReplaceSpaces(version)
81+
}
82+
}
83+
84+
func ReplaceSpacesUseBuilder(s string) string {
85+
// Pre-allocate a builder with the same length as s to minimize allocations.
86+
// This is a basic optimization; adjust the initial size based on your use case.
87+
var builder strings.Builder
88+
builder.Grow(len(s))
89+
90+
for _, char := range s {
91+
if char == ' ' {
92+
// Replace space with a hyphen.
93+
builder.WriteRune('-')
94+
} else {
95+
// Copy the character as-is.
96+
builder.WriteRune(char)
97+
}
98+
}
99+
100+
return builder.String()
101+
}
102+
103+
func BenchmarkReplaceSpacesUseBuilder(b *testing.B) {
104+
version := runtime.Version()
105+
for i := 0; i < b.N; i++ {
106+
_ = ReplaceSpacesUseBuilder(version)
107+
}
108+
}

internal_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,14 +364,14 @@ var _ = Describe("ClusterClient", func() {
364364
It("select slot from args for GETKEYSINSLOT command", func() {
365365
cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200)
366366

367-
slot := client.cmdSlot(context.Background(), cmd)
367+
slot := client.cmdSlot(cmd)
368368
Expect(slot).To(Equal(100))
369369
})
370370

371371
It("select slot from args for COUNTKEYSINSLOT command", func() {
372372
cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100)
373373

374-
slot := client.cmdSlot(context.Background(), cmd)
374+
slot := client.cmdSlot(cmd)
375375
Expect(slot).To(Equal(100))
376376
})
377377
})

main_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ var _ = BeforeSuite(func() {
100100

101101
fmt.Printf("RECluster: %v\n", RECluster)
102102
fmt.Printf("RCEDocker: %v\n", RCEDocker)
103-
fmt.Printf("REDIS_VERSION: %v\n", RedisVersion)
103+
fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion)
104+
fmt.Printf("CLIENT_LIBS_TEST_IMAGE: %v\n", os.Getenv("CLIENT_LIBS_TEST_IMAGE"))
104105

105106
if RedisVersion < 7.0 || RedisVersion > 9 {
106107
panic("incorrect or not supported redis version")

0 commit comments

Comments
 (0)