Repository: redis/go-redis

Issue:
Make shards in redis.Ring publicly accessible
We use `redis.Ring` to shard load across servers. We shard mainly PubSub related commands,  `PSUBSCRIBE`, `SUBSCRIBE`, `PUNSUBSCRIBE` and `UNSUBSCRIBE`.

Because these commands put the redis connection in a special "pubsub" mode, after which you cannot use it for anything else than pubsub, we try to re-use these connections as much as possible.

What we'd like to do is set up a pubsub connection for every shard in the ring at application start, and then re-use that pubsub connection when we need to subscribe or unsubscribe on a topic.

However for that to be possible, we need two things:

 * access to the list of shards, so we can immediately create a pubsub connection for each shard in the ring
 * access to `ringSharding.GetByKey` so we can determine which shard pubsub connection we can re-use for a given topic

However, on `redis.Ring` the `sharding` attribute, which contains an instance of `*ringSharding`, is not publicly accessible, so we cannot access the shards list or the GetByKey function.

We are working around this for now, but it's rather ugly, because we need to make assumptions in our own code on how topics are distributed over the ring.

Would it be possible to make `redis.Ring.sharding` public, or at least provide some public methods on `redis.Ring` to return the list of shards access the `GetByKey` function?

I would be open to create a merge request if there is a chance of it being accepted.

Discussion:
@ndyakov: Hello @jaapz , as far as I remember, the distribution of keys for Ring will be done with consistent hashing and you can even provide the algorithm. @htemelski maybe you can take a look if it makes sense to export the ring sharding. @jaapz: Correct, we're using that as well. But if we want to determine on our side which server will be returned for a given topic, then we need to basically copy code that is already present in redis.Ring. It would be nicer if we could simply call something like `redis.Ring.Sharding.GetByKey(mytopic)` or `redis.Ring.GetByKey(mytopic)` and get the shard like that.@ndyakov: @ofekshenawa if you have the time you may look into this and if it makes sense to be exposed. 

Solution patch:
diff --git a/ring.go b/ring.go
index fe8a6dc..8a004b8 100644
--- a/ring.go
+++ b/ring.go
@@ -847,3 +847,26 @@ func (c *Ring) Close() error {
 
 	return c.sharding.Close()
 }
+
+// GetShardClients returns a list of all shard clients in the ring.
+// This can be used to create dedicated connections (e.g., PubSub) for each shard.
+func (c *Ring) GetShardClients() []*Client {
+	shards := c.sharding.List()
+	clients := make([]*Client, 0, len(shards))
+	for _, shard := range shards {
+		if shard.IsUp() {
+			clients = append(clients, shard.Client)
+		}
+	}
+	return clients
+}
+
+// GetShardClientForKey returns the shard client that would handle the given key.
+// This can be used to determine which shard a particular key/channel would be routed to.
+func (c *Ring) GetShardClientForKey(key string) (*Client, error) {
+	shard, err := c.sharding.GetByKey(key)
+	if err != nil {
+		return nil, err
+	}
+	return shard.Client, nil
+}



Test patch:
diff --git a/ring_test.go b/ring_test.go
index 599f688..aaac74d 100644
--- a/ring_test.go
+++ b/ring_test.go
@@ -782,3 +782,82 @@ var _ = Describe("Ring Tx timeout", func() {
 		testTimeout()
 	})
 })
+
+var _ = Describe("Ring GetShardClients and GetShardClientForKey", func() {
+	var ring *redis.Ring
+
+	BeforeEach(func() {
+		ring = redis.NewRing(&redis.RingOptions{
+			Addrs: map[string]string{
+				"shard1": ":6379",
+				"shard2": ":6380",
+			},
+		})
+	})
+
+	AfterEach(func() {
+		Expect(ring.Close()).NotTo(HaveOccurred())
+	})
+
+	It("GetShardClients returns active shard clients", func() {
+		shards := ring.GetShardClients()
+		// Note: This test will pass even if Redis servers are not running,
+		// because GetShardClients only returns clients that are marked as "up",
+		// and newly created shards start as "up" until the first health check fails.
+
+		if len(shards) == 0 {
+			// Expected if Redis servers are not running
+			Skip("No active shards found (Redis servers not running)")
+		} else {
+			Expect(len(shards)).To(BeNumerically(">", 0))
+			for _, client := range shards {
+				Expect(client).NotTo(BeNil())
+			}
+		}
+	})
+
+	It("GetShardClientForKey returns correct shard for keys", func() {
+		testKeys := []string{"key1", "key2", "user:123", "channel:test"}
+
+		for _, key := range testKeys {
+			client, err := ring.GetShardClientForKey(key)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(client).NotTo(BeNil())
+		}
+	})
+
+	It("GetShardClientForKey is consistent for same key", func() {
+		key := "test:consistency"
+
+		// Call GetShardClientForKey multiple times with the same key
+		// Should always return the same shard
+		var firstClient *redis.Client
+		for i := 0; i < 5; i++ {
+			client, err := ring.GetShardClientForKey(key)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(client).NotTo(BeNil())
+
+			if i == 0 {
+				firstClient = client
+			} else {
+				Expect(client.String()).To(Equal(firstClient.String()))
+			}
+		}
+	})
+
+	It("GetShardClientForKey distributes keys across shards", func() {
+		testKeys := []string{"key1", "key2", "key3", "key4", "key5"}
+		shardMap := make(map[string]int)
+
+		for _, key := range testKeys {
+			client, err := ring.GetShardClientForKey(key)
+			Expect(err).NotTo(HaveOccurred())
+			shardMap[client.String()]++
+		}
+
+		// Should have at least 1 shard (could be all keys go to same shard due to hashing)
+		Expect(len(shardMap)).To(BeNumerically(">=", 1))
+		// But with multiple keys, we expect some distribution
+		Expect(len(shardMap)).To(BeNumerically("<=", 2)) // At most 2 shards (our setup)
+	})
+})