Skip to content

Commit 7dc3c24

Browse files
committed
fix: prevent panic in distributeWithLoad and add regression tests #13
- Fixed `distributeWithLoad` to ensure all ring positions are checked, preventing a panic with certain configurations. - Added regression tests to ensure correct behavior and expected panics in edge cases.
1 parent 9b24b48 commit 7dc3c24

2 files changed

Lines changed: 82 additions & 2 deletions

File tree

consistent.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,10 @@ func (c *Consistent) distributeWithLoad(partID, idx int, partitions map[int]*Mem
186186
var count int
187187
for {
188188
count++
189-
if count >= len(c.sortedSet) {
190-
// User needs to decrease partition count, increase member count or increase load factor.
189+
if count > len(c.sortedSet) {
190+
// We checked all positions on the ring. None of them has free space.
191+
// User needs to decrease the partition count, increase the member count
192+
// or increase the load factor.
191193
panic("not enough room to distribute partitions")
192194
}
193195
i := c.sortedSet[idx]

consistent_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,84 @@ func TestConsistentLoad(t *testing.T) {
123123
})
124124
}
125125

126+
// Regression test for https://github.com/buraksezer/consistent/issues/13
127+
//
128+
// distributeWithLoad increased the counter before it checked a position on the
129+
// ring. So the last position was never checked. A ring with one position always
130+
// panicked, even if the member was empty.
131+
func TestConsistentDistributeWithLoadCoversWholeRing(t *testing.T) {
132+
newMembers := func(count int) []Member {
133+
var members []Member
134+
for i := 0; i < count; i++ {
135+
members = append(members, testMember(fmt.Sprintf("node%d.olric", i)))
136+
}
137+
return members
138+
}
139+
140+
cases := []struct {
141+
name string
142+
memberCount int
143+
config Config
144+
}{
145+
{
146+
name: "single member on a single ring position",
147+
memberCount: 1,
148+
config: Config{PartitionCount: 100, ReplicationFactor: 1, Load: 1.25, Hasher: hasher{}},
149+
},
150+
{
151+
name: "one partition on one member",
152+
memberCount: 1,
153+
config: Config{PartitionCount: 1, ReplicationFactor: 1, Load: 1, Hasher: hasher{}},
154+
},
155+
{
156+
name: "few partitions over few members without replicas",
157+
memberCount: 2,
158+
config: Config{PartitionCount: 10, ReplicationFactor: 1, Load: 1.1, Hasher: hasher{}},
159+
},
160+
{
161+
name: "default configuration",
162+
memberCount: 8,
163+
config: Config{PartitionCount: 271, ReplicationFactor: 20, Load: 1.25, Hasher: hasher{}},
164+
},
165+
}
166+
167+
for _, tc := range cases {
168+
t.Run(tc.name, func(t *testing.T) {
169+
defer func() {
170+
if err := recover(); err != nil {
171+
t.Fatalf("Expected no panic, Got: %v", err)
172+
}
173+
}()
174+
175+
c := New(newMembers(tc.memberCount), tc.config)
176+
// Every partition must have an owner.
177+
for partID := 0; partID < tc.config.PartitionCount; partID++ {
178+
if c.GetPartitionOwner(partID) == nil {
179+
t.Fatalf("partition %d has no owner", partID)
180+
}
181+
}
182+
})
183+
}
184+
}
185+
186+
// The panic must still work after the fix above. If there is no room for all
187+
// partitions, distributeWithLoad has to panic, not loop forever.
188+
func TestConsistentDistributeWithLoadExhausted(t *testing.T) {
189+
defer func() {
190+
if err := recover(); err == nil {
191+
t.Fatal("Expected a panic, Got: nil")
192+
}
193+
}()
194+
195+
var members []Member
196+
for i := 0; i < 2; i++ {
197+
members = append(members, testMember(fmt.Sprintf("node%d.olric", i)))
198+
}
199+
// Average load is Ceil(10/2 * 0.5) = 3. Two members can take 6 partitions,
200+
// but we have 10 of them.
201+
New(members, Config{PartitionCount: 10, ReplicationFactor: 1, Load: 0.5, Hasher: hasher{}})
202+
}
203+
126204
func TestConsistentLocateKey(t *testing.T) {
127205
cfg := newConfig()
128206
c := New(nil, cfg)

0 commit comments

Comments
 (0)