tcredis

package module
v0.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 17, 2026 License: MIT Imports: 15 Imported by: 0

README

testcontainers-go-redis-cluster

A Go library for creating Redis clusters in tests using testcontainers-go. Provides two implementations: Redis (V1) and RedisV2 (recommended, faster).

Quick Start

package main

import (
    "context"
    "testing"

    "github.com/NSXBet/testcontainers-go-redis-cluster"
    "github.com/redis/go-redis/v9"
)

func TestMyFeature(t *testing.T) {
    // RECOMMENDED: Use RedisV3 for fast, reliable cluster testing (~1s startup, 100% reliable)
    connStr := tcredis.RedisV3(t) // Single-node emulated cluster (Dragonfly)

    // Alternative: Use RedisV2 for multi-node cluster testing (~8s startup)
    // Note: RedisV2 has ~50% reliability due to Redis gossip protocol issues in Docker
    // connStr := tcredis.RedisV2(t, 3) // 3-node cluster
    
    // Parse connection string and create cluster client
    opts, err := redis.ParseClusterURL(connStr)
    if err != nil {
        t.Fatalf("Failed to parse connection string: %v", err)
    }
    
    client := redis.NewClusterClient(opts)
    defer client.Close()
    
    // Use the cluster client
    ctx := context.Background()
    err = client.Set(ctx, "key", "value", 0).Err()
    if err != nil {
        t.Fatalf("Failed to set key: %v", err)
    }
    
    val, err := client.Get(ctx, "key").Result()
    if err != nil {
        t.Fatalf("Failed to get key: %v", err)
    }
    
    t.Logf("Got value: %s", val)
}

API

Creates a Redis-compatible cluster using Dragonfly in emulated cluster mode. This is the most reliable and fastest option for testcontainers.

Implementation: Uses Dragonfly (a high-performance Redis alternative) which presents itself as a Redis cluster to clients, but internally it's a single powerful node. This architecture completely avoids the cluster-announce propagation issues that plague multi-container Redis clusters.

Parameters:

  • t: The testing.TB instance (used for cleanup). Accepts both *testing.T (unit tests) and *testing.B (benchmarks).

Returns: Connection string in redis://host:port format compatible with redis.ParseClusterURL()

How it works:

  1. Starts a single Dragonfly container with --cluster_mode=emulated
  2. Dragonfly responds to all Redis Cluster protocol commands
  3. No inter-node communication = no cluster-announce propagation issues
  4. Verifies readiness before returning

Benchmarks:

  • Startup time: ~1 second (vs 8s for V2, 22s for V1)
  • 🎯 Reliability: 100% in testing (141/141 tests passed)
  • 🚀 Performance: 25x faster than Redis
  • 📦 Single container: No networking complexity

Ports: Uses port 26379

Trade-offs:

  • ✅ Extremely fast and reliable
  • ✅ 25x performance improvement over Redis
  • ✅ Perfect for most testing scenarios
  • ❌ Single node (can't test multi-node cluster scenarios like failover, resharding)
  • ❌ Not testing real Redis (different implementation)

When to use:

  • Unit and integration tests
  • Performance testing
  • Any test that needs Redis Cluster protocol but not actual distributed behavior
  • When you need speed and reliability

When NOT to use:

  • Testing actual multi-node cluster behavior (failover, resharding, replication)
  • When you specifically need to test against Redis (not Dragonfly)

tcredis.RedisV2(t testing.TB, nodes int) string

Creates a Redis cluster by starting individual Redis containers and manually forming the cluster. This approach is significantly faster (~8 seconds) than V1.

Parameters:

  • t: The testing.TB instance (used for cleanup). Accepts both *testing.T (unit tests) and *testing.B (benchmarks).
  • nodes: Number of master nodes in the cluster (must be >= 3)

Returns: Connection string in redis://host:port format compatible with redis.ParseClusterURL()

How it works:

  1. Starts N separate Redis containers (one per master node)
  2. Creates a Docker network for inter-container communication
  3. Forms the cluster using redis-cli --cluster create
  4. Configures cluster-announce-ip and cluster-announce-port for external client access
  5. Verifies cluster readiness and returns connection string

Performance: ~8 seconds for a 3-node cluster (vs ~22 seconds for V1)

Ports: Uses ports starting from 7000 (7000, 7001, 7002, etc.)

tcredis.Redis(t testing.TB, nodes int) string

Creates a Redis cluster using the grokzen/redis-cluster Docker image. This is the original implementation.

Parameters:

  • t: The testing.TB instance (used for cleanup). Accepts both *testing.T (unit tests) and *testing.B (benchmarks).
  • nodes: Number of master nodes in the cluster (must be >= 3). Each master will have 1 slave by default.

Returns: Connection string in redis://host:port format compatible with redis.ParseClusterURL()

How it works:

  1. Automatically builds a local Docker image (tcredis/redis-cluster:7.0.7) on first use
  2. Starts a single container with the grokzen/redis-cluster image
  3. The image automatically sets up the cluster with masters and slaves
  4. Configures cluster-announce-ip and cluster-announce-port for external client access
  5. Verifies cluster readiness and returns connection string

Performance: ~22 seconds for a 3-node cluster

Note: The number of nodes specified is the number of masters. The total number of nodes will be 2 * nodes (masters + slaves). For example, Redis(t, 5) creates 5 masters and 5 slaves (10 nodes total).

Ports: Uses ports starting from 10000 (10000 to 10000 + 2 * nodes - 1)

Comparison: RedisV3 vs RedisV2 vs Redis

Benchmark Results
Metric RedisV3 (⭐ RECOMMENDED) RedisV2 Redis (V1)
Implementation Dragonfly emulated Multi-container Redis Single container grokzen
Startup Time ~1 second ⚡ ~8 seconds ~22 seconds
Reliability 100% (141/141) 30-55% (gossip issues) 100% (single container)
Performance 25x faster than Redis Same as Redis Same as Redis
Nodes 1 (emulated cluster) N masters (no replicas) N masters + N replicas
Port 26379 27000+ 10000+
Cleanup ✅ Perfect ✅ Perfect ✅ Perfect
Multi-node Testing ❌ No ✅ Yes ✅ Yes
Reliability Testing Results

Extensive testing with 20-50 iterations per implementation:

RedisV3:  141/141 tests passed = 100.0% ✅⚡
Redis V1: ~100% reliable (single container, no network issues)
RedisV2:  30-55% reliable (Redis gossip protocol non-determinism in Docker)
Startup Time Benchmarks

Average startup time until cluster is ready for client operations:

RedisV3:  0.8-1.8 seconds  (22x faster than V1, 8x faster than V2) ⚡
RedisV2:  6-11 seconds     (2.7x faster than V1)
Redis V1: 20-24 seconds    (baseline)
When to Use Each Implementation
Use Case Recommended Why
Unit tests RedisV3 Fastest, most reliable
Integration tests RedisV3 Fast iteration, full cluster protocol support
CI/CD pipelines RedisV3 Speed + reliability = faster builds
Multi-node cluster testing Redis V1 Need real distributed behavior
Failover/resharding tests Redis V1 Need multiple actual nodes
Replica behavior testing Redis V1 Only V1 has replicas
Fast multi-node iteration RedisV2 When V1 too slow, can tolerate flakiness

Recommendations:

  • 🥇 Use RedisV3 for 95% of tests - fastest and most reliable
  • 🥈 Use Redis (V1) when you specifically need multi-node behavior or replicas
  • 🥉 Use RedisV2 rarely - only when you need multi-node testing and V1 is too slow

Parallel Test Execution

All implementations support parallel test execution without port conflicts. The library uses an internal port allocator that:

  1. Allocates ports sequentially starting from 27000 (configurable)
  2. No port reuse - always allocates new sequential ports to avoid Docker async cleanup conflicts
  3. Thread-safe for concurrent test execution

Why no port reuse? Docker's cleanup is asynchronous. Even after container.Terminate() returns, Docker may spend several seconds cleaning up network bindings. Reusing ports too quickly causes "port is already allocated" errors.

Customizing Ports

You can customize ports in two ways:

1. Per-test (Recommended) - Using functional options:

// RedisV3 with custom port
connStr := tcredis.RedisV3(t, tcredis.WithStartingPort(38000))

// RedisV2 with custom port range
connStr := tcredis.RedisV2(t, 3, tcredis.WithStartingPortV2(39000))
// Uses ports 39000, 39001, 39002

2. Global - Using SetStartingPort() in TestMain:

func TestMain(m *testing.M) {
    // All tests will use ports starting from 37000
    tcredis.SetStartingPort(37000)
    os.Exit(m.Run())
}

Recommendation: Use functional options (WithStartingPort) for flexibility. Use SetStartingPort() only if you need to change the default for all tests.

Port Allocation Behavior
  • RedisV3: Allocates 1 port per instance (reused after cleanup)
  • RedisV2: Allocates N contiguous ports for N nodes (e.g., 27000-27002 for 3 nodes)
  • Redis (V1): Allocates 2*N ports for N masters + N replicas (e.g., 10000-10005 for 3 masters)

Example parallel execution:

// Test 1 gets port 27000
// Test 2 gets port 27001 (parallel or sequential)
// Test 3 gets port 27002
// Test 4 gets port 27003
// etc.
// Ports are NOT reused to avoid Docker async cleanup conflicts

Requirements

  • Go 1.21 or later
  • Docker (required by testcontainers-go)

Connection String Format

Both functions return connection strings in the format:

redis://host:port?addr=host:port2&addr=host:port3

This format is compatible with redis.ParseClusterURL() from the github.com/redis/go-redis/v9 package.

Automatic Image Building (Redis V1 only)

The Redis function automatically builds a local Docker image (tcredis/redis-cluster:7.0.7) on first use. This ensures:

  • Better ARM64 compatibility: No emulation needed on Apple Silicon
  • Faster subsequent runs: Image is cached after first build
  • More reliable: Locally built images work better with testcontainers

First run: The library will automatically:

  1. Check if tcredis/redis-cluster:7.0.7 exists locally
  2. If not, clone the grokzen/docker-redis-cluster repository (cached in /tmp/docker-redis-cluster)
  3. Build the image for your native platform (arm64 on Apple Silicon, amd64 on Intel)
  4. Use the locally built image

Subsequent runs: The image is already built, so tests start immediately.

The repository is cached in /tmp/docker-redis-cluster and will be updated automatically when needed.

Note: RedisV2 uses the official redis:7.0.7 image which is pulled automatically if not present.

Cluster Requirements

  • Minimum nodes: Redis clusters require at least 3 master nodes to form a proper cluster with hash slot assignment. Both Redis and RedisV2 enforce this requirement and will fail with a clear error message if fewer than 3 nodes are requested.

Testing

Run the tests:

go test ./...

Make sure Docker is running before executing tests.

Examples

func TestRedisCluster(t *testing.T) {
    connStr := tcredis.RedisV3(t) // Dragonfly emulated cluster - fast and reliable!

    opts, _ := redis.ParseClusterURL(connStr)
    client := redis.NewClusterClient(opts)
    defer client.Close()

    ctx := context.Background()
    client.Set(ctx, "key", "value", 0)
    val, _ := client.Get(ctx, "key").Result()
    t.Logf("Value: %s", val)
}
Using RedisV2
func TestRedisCluster(t *testing.T) {
    connStr := tcredis.RedisV2(t, 3) // 3-node cluster
    
    opts, _ := redis.ParseClusterURL(connStr)
    client := redis.NewClusterClient(opts)
    defer client.Close()
    
    ctx := context.Background()
    client.Set(ctx, "key", "value", 0)
    val, _ := client.Get(ctx, "key").Result()
    t.Logf("Value: %s", val)
}
Using Redis (V1)
func TestRedisCluster(t *testing.T) {
    connStr := tcredis.Redis(t, 3) // 3 masters + 3 slaves = 6 nodes total
    
    opts, _ := redis.ParseClusterURL(connStr)
    client := redis.NewClusterClient(opts)
    defer client.Close()
    
    ctx := context.Background()
    client.Set(ctx, "key", "value", 0)
    val, _ := client.Get(ctx, "key").Result()
    t.Logf("Value: %s", val)
}

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Redis

func Redis(t testing.TB, nodes int) string

Redis sets up a Redis cluster with the specified number of master nodes. It returns a connection string in the format redis://host:port that can be used with Redis clients that support cluster mode. The cluster is automatically cleaned up when the test completes via t.Cleanup(). Based on https://github.com/Grokzen/docker-redis-cluster

func RedisV2

func RedisV2(t testing.TB, nodes int, options ...RedisV2Option) string

RedisV2 sets up a Redis cluster by starting N separate Redis containers and configuring them to form a cluster. This approach is faster than using the grokzen image since individual Redis containers start very quickly. It returns a connection string in the format redis://host:port that can be used with Redis clients that support cluster mode.

Options: - WithStartingPortV2(port): Use a specific starting port instead of auto-allocation

Note: RedisV2 has 30-55% reliability due to Redis gossip protocol issues in Docker. For production tests, use RedisV3 (100% reliable) or Redis V1 (100% reliable with replicas).

func RedisV3 added in v0.2.0

func RedisV3(t testing.TB, options ...RedisV3Option) string

RedisV3 sets up a Redis-compatible cluster using Dragonfly's emulated cluster mode. This is the RECOMMENDED implementation for most testing scenarios.

Implementation: Uses Dragonfly (a high-performance Redis alternative) in emulated cluster mode. Dragonfly presents itself as a Redis cluster to clients but internally it's a single powerful node. This architecture completely avoids cluster-announce propagation issues that affect multi-container Redis deployments.

Advantages: - Ultra-fast startup (~1 second vs 8s for V2, 22s for V1) - High reliability (100% pass rate in testing) - Full Redis Cluster protocol compatibility - Exceptional performance (25x faster than Redis) - No Docker networking complexity - Parallel test safe (automatic port allocation)

Trade-offs: - Single node (can't test multi-node scenarios like failover, resharding) - Uses Dragonfly instead of actual Redis (different implementation)

Options: - WithStartingPort(port): Use a specific port instead of auto-allocation

The container is automatically cleaned up when the test completes via t.Cleanup().

func SetStartingPort added in v0.2.1

func SetStartingPort(port int)

SetStartingPort sets the starting port for the global port allocator This must be called before any tests run (e.g., in TestMain or init) Subsequent calls have no effect

Note: For test-specific port configuration, prefer using WithStartingPort() or WithStartingPortV2() options instead of this global setting.

Types

type RedisV2Option added in v0.2.1

type RedisV2Option func(*RedisV2Options)

RedisV2Option is a functional option for configuring RedisV2

func WithStartingPortV2 added in v0.2.1

func WithStartingPortV2(port int) RedisV2Option

WithStartingPortV2 sets a custom base port for the port allocator The allocator will start allocating from this port instead of the default 27000 This affects ALL subsequent tests, not just this one Example: WithStartingPortV2(7000) means tests will use 7000-7002, 7003-7005, etc.

type RedisV2Options added in v0.2.1

type RedisV2Options struct {
	// contains filtered or unexported fields
}

RedisV2Options configures RedisV2 behavior

type RedisV3Option added in v0.2.1

type RedisV3Option func(*RedisV3Options)

RedisV3Option is a functional option for configuring RedisV3

func WithStartingPort added in v0.2.1

func WithStartingPort(port int) RedisV3Option

WithStartingPort sets a custom base port for the port allocator The allocator will start allocating from this port instead of the default 27000 This affects ALL subsequent tests, not just this one Example: WithStartingPort(7000) means tests will use 7000, 7001, 7002, etc.

type RedisV3Options added in v0.2.1

type RedisV3Options struct {
	// contains filtered or unexported fields
}

RedisV3Options configures RedisV3 behavior

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL