From d33477c02a0ef9f290a2e83f5c2babcc6e2a97d0 Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Sat, 10 Jan 2026 15:30:58 +0100 Subject: [PATCH] Add mutex protection to ConsistentHashRing for thread safety - Add sync.RWMutex to ConsistentHashRing struct - Use Lock/Unlock for write operations (AddNode, RemoveNode) - Use RLock/RUnlock for read operations (GetNode, GetNodes, IsEmpty) This allows concurrent reads (the common case) while serializing writes, preventing race conditions when multiple goroutines access the hash ring. Closes #35 Co-Authored-By: Claude Opus 4.5 --- cluster/hashring.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cluster/hashring.go b/cluster/hashring.go index f1c03c3..0208b45 100644 --- a/cluster/hashring.go +++ b/cluster/hashring.go @@ -5,10 +5,12 @@ import ( "encoding/binary" "fmt" "sort" + "sync" ) // ConsistentHashRing implements a consistent hash ring for shard distribution type ConsistentHashRing struct { + mu sync.RWMutex ring map[uint32]string // hash -> node ID sortedHashes []uint32 // sorted hash keys nodes map[string]bool // active nodes @@ -24,6 +26,9 @@ func NewConsistentHashRing() *ConsistentHashRing { // AddNode adds a node to the hash ring func (chr *ConsistentHashRing) AddNode(nodeID string) { + chr.mu.Lock() + defer chr.mu.Unlock() + if chr.nodes[nodeID] { return // Node already exists } @@ -45,6 +50,9 @@ func (chr *ConsistentHashRing) AddNode(nodeID string) { // RemoveNode removes a node from the hash ring func (chr *ConsistentHashRing) RemoveNode(nodeID string) { + chr.mu.Lock() + defer chr.mu.Unlock() + if !chr.nodes[nodeID] { return // Node doesn't exist } @@ -65,6 +73,9 @@ func (chr *ConsistentHashRing) RemoveNode(nodeID string) { // GetNode returns the node responsible for a given key func (chr *ConsistentHashRing) GetNode(key string) string { + chr.mu.RLock() + defer chr.mu.RUnlock() + if len(chr.sortedHashes) == 0 { return "" } @@ -92,6 +103,9 @@ func (chr *ConsistentHashRing) hash(key string) uint32 { // GetNodes returns all active nodes in the ring func (chr *ConsistentHashRing) GetNodes() []string { + chr.mu.RLock() + defer chr.mu.RUnlock() + nodes := make([]string, 0, len(chr.nodes)) for nodeID := range chr.nodes { nodes = append(nodes, nodeID) @@ -101,5 +115,8 @@ func (chr *ConsistentHashRing) GetNodes() []string { // IsEmpty returns true if the ring has no nodes func (chr *ConsistentHashRing) IsEmpty() bool { + chr.mu.RLock() + defer chr.mu.RUnlock() + return len(chr.nodes) == 0 -} \ No newline at end of file +}