1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-22 19:49:00 +03:00

test(tests): TCP-probe before Ping in skip gates (#4001)

Tests that skip when Redis is absent gated on a client Ping to a
hardcoded/helper address. A Ping to an addr with nothing listening does
not fail fast: it pays the dial-retry (DialerRetries, default 5) plus
command-retry budget first, ~1.7s per call. A recent RE integration
nightly spent ~1.6 min across 68 such skips.

Probe the address with a short-timeout TCP dial first, which fails in
microseconds on connection-refused, then keep the existing Ping so a
reachable-but-unusable server still skips rather than proceeds.
This commit is contained in:
Nedyalko Dyakov
2026-08-31 14:18:10 +03:00
committed by GitHub
parent 6c87f94116
commit c5cad058c7
10 changed files with 144 additions and 0 deletions

View File

@@ -42,6 +42,9 @@ func TestAutoPipelineLoneCallerFlushesImmediately(t *testing.T) {
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -97,6 +100,9 @@ func TestAutoPipelineExplicitDelayWaitsFullWindow(t *testing.T) {
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -192,6 +198,9 @@ func TestAutoPipelineWaveCoalesces(t *testing.T) {
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -322,6 +331,9 @@ func TestDoBypassesPipeline(t *testing.T) {
client := NewClient(&Options{Addr: internalTestRedisAddr(), PipelineReadBufferSize: 64 << 10, PipelineWriteBufferSize: 64 << 10})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -564,6 +576,9 @@ func TestDispatchChunkedAbortsAfterFailedPrefix(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr(), MaxRetries: -1})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -764,6 +779,9 @@ func TestCloseWaitsForDivertedCommand(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -794,6 +812,9 @@ func TestCloseWaitsForDivertedCommand(t *testing.T) {
// opposite in its doc).
func TestSharedClosedRejectsEveryEntryPoint(t *testing.T) {
ctx := context.Background()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
parent := NewClient(&Options{Addr: internalTestRedisAddr()})
defer parent.Close()
if err := parent.Ping(ctx).Err(); err != nil {
@@ -863,6 +884,9 @@ func TestDivertRegistrationRacesClose(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -933,6 +957,9 @@ func TestOtelMetricsDoNotAwaitOnAsyncFace(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1036,6 +1063,9 @@ func TestEvalDoesNotAwaitOnAsyncFace(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1191,6 +1221,9 @@ func TestNoRetryOrderObservedEndToEnd(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1227,6 +1260,9 @@ func TestNoRetrySplitExecutesEveryCommand(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1308,6 +1344,9 @@ func TestSuccessfulHookShortCircuitIsHonored(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1370,6 +1409,9 @@ func TestMustDivertKeepsCommandOffTheBatchPath(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1418,6 +1460,9 @@ func TestRetryRunsStopAfterFailedPrefix(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr(), MaxRetries: -1})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1459,6 +1504,9 @@ func TestAsyncProcessReportsSubmitRejection(t *testing.T) {
ctx := context.Background()
client := NewClient(&Options{Addr: internalTestRedisAddr()})
defer client.Close()
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := client.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -1507,6 +1555,9 @@ func TestAutopipelineSoloRoutingGate(t *testing.T) {
}
// CSC client: needs a live RESP3 server for CLIENT TRACKING to attach.
if err := probeRedis(internalTestRedisAddr()); err != nil {
t.Skipf("no redis for the CSC half: %v", err)
}
if err := NewClient(&Options{Addr: internalTestRedisAddr(), Protocol: 3}).Ping(ctx).Err(); err != nil {
t.Skipf("no redis for the CSC half: %v", err)
}

View File

@@ -321,6 +321,9 @@ func TestCSCNonZeroDBRejected(t *testing.T) {
t.Cleanup(func() { _ = c.Close() })
ctx := context.Background()
if err := probeRedis(cscNativeAddr()); err != nil {
t.Skipf("redis not available at %s: %v", cscNativeAddr(), err)
}
if err := c.Ping(ctx).Err(); err != nil {
t.Skipf("redis not available at %s: %v", cscNativeAddr(), err)
}
@@ -371,6 +374,9 @@ func TestCSCReadYourWrites(t *testing.T) {
t.Cleanup(func() { _ = mutator.Close() })
ctx := context.Background()
if err := probeRedis(cscNativeAddr()); err != nil {
t.Skipf("redis not available at %s: %v", cscNativeAddr(), err)
}
if err := c.Ping(ctx).Err(); err != nil {
t.Skipf("redis not available at %s: %v", cscNativeAddr(), err)
}

View File

@@ -30,6 +30,9 @@ func TestDigestBasic(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -74,6 +77,9 @@ func TestSetIFDEQWithDigest(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -138,6 +144,9 @@ func TestSetIFDNEWithDigest(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -202,6 +211,9 @@ func TestDelExArgsWithDigest(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -267,6 +279,9 @@ func TestDigestHelperMatchesRedis(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -326,6 +341,9 @@ func TestDigestBytesHelperMatchesRedis(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -382,6 +400,9 @@ func TestDigestHelperWithSetIFDEQ(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
@@ -439,6 +460,9 @@ func TestDigestHelperWithDelExArgs(t *testing.T) {
skipIfRedisBelow84(t)
ctx := context.Background()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

View File

@@ -11,6 +11,9 @@ import (
func newLanguagesTestClient(t *testing.T) *redis.Client {
t.Helper()
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

View File

@@ -194,6 +194,9 @@ func TestPipelinePoolSpillsToMainPool(t *testing.T) {
PoolTimeout: 100 * time.Millisecond, // spill latency bound
})
defer c.Close()
if err := probeRedis(gateTestAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := c.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}
@@ -265,6 +268,9 @@ func TestPipelineConnsSkipClientTracking(t *testing.T) {
ClientSideCacheConfig: &ClientSideCacheConfig{},
})
defer c.Close()
if err := probeRedis(gateTestAddr()); err != nil {
t.Skipf("no redis: %v", err)
}
if err := c.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}

View File

@@ -29,6 +29,9 @@ func TestWithPipelineConnSpillAccountsLimiterOnce(t *testing.T) {
lim := &spillCountingLimiter{}
c := NewClient(&Options{Addr: ":6379", PipelinePoolSize: 1, Limiter: lim})
defer c.Close()
if err := probeRedis(":6379"); err != nil {
t.Skipf("no redis: %v", err)
}
if err := c.Ping(ctx).Err(); err != nil {
t.Skipf("no redis: %v", err)
}

18
probe_internal_test.go Normal file
View File

@@ -0,0 +1,18 @@
package redis
import (
"net"
"time"
)
// probeRedis is the package redis copy of the redis_test helper of the same
// name (the internal tests cannot import the external test package). See
// probe_test.go for the rationale: a fast TCP probe avoids the ~1.7s
// dial-retry + command-retry cost a Ping pays when nothing is listening.
func probeRedis(addr string) error {
c, err := net.DialTimeout("tcp", addr, 300*time.Millisecond)
if err != nil {
return err
}
return c.Close()
}

27
probe_test.go Normal file
View File

@@ -0,0 +1,27 @@
package redis_test
import (
"net"
"time"
)
// probeRedis reports whether a TCP listener answers at addr within a short
// timeout. Tests that skip when Redis is absent call this before building a
// client and Ping-ing it: a Ping to an addr with nothing listening does not
// return immediately, it pays go-redis's dial-retry (DialerRetries, 5 by
// default) plus command-retry (MaxRetries) budget first — ~1.7s per call. In an
// environment without a local Redis (for example the RE nightly, whose real
// endpoint is remote), hundreds of such skips add up to minutes of wasted wall
// time. A single short-timeout TCP dial fails in microseconds on
// connection-refused instead.
//
// A nil return only means something is listening; callers keep their Ping so a
// reachable-but-unusable server (auth required, a different service on the port)
// still skips rather than proceeds.
func probeRedis(addr string) error {
c, err := net.DialTimeout("tcp", addr, 300*time.Millisecond)
if err != nil {
return err
}
return c.Close()
}

View File

@@ -41,6 +41,9 @@ func TestFTAggregateCollect_Integration(t *testing.T) {
// REDIS_VERSION env variable (whose float form cannot tell 8.10 from 8.1
// and defaults when unset).
probeCtx := context.Background()
if err := probeRedis(collectTestAddr()); err != nil {
t.Skipf("redis not reachable at %s: %v", collectTestAddr(), err)
}
probe := redis.NewClient(&redis.Options{Addr: collectTestAddr()})
t.Cleanup(func() { _ = probe.Close() })
if err := probe.Ping(probeCtx).Err(); err != nil {

View File

@@ -28,6 +28,9 @@ func TestSearchCommandsRESP2AndRESP3Equivalence(t *testing.T) {
defer client3.Close()
// Check connection
if err := probeRedis("localhost:6379"); err != nil {
t.Skipf("Redis not available: %v", err)
}
if err := client2.Ping(ctx).Err(); err != nil {
t.Skipf("Redis not available: %v", err)
}