128 lines
3.9 KiB
Go
128 lines
3.9 KiB
Go
package kafka
|
|||
|
|
|
||
|
|
import "testing"
|
||
|
|
|
||
|
|
func TestShardTopicName(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
queue string
|
||
|
|
fifo bool
|
||
|
|
shard string
|
||
|
|
want string
|
||
|
|
}{
|
||
|
|
{"standard", "orders", false, "0", "kmsvc.orders.shard-0"},
|
||
|
|
{"fifo", "orders", true, "0", "kmsvc.orders.fifo.shard-0"},
|
||
|
|
{"split child", "orders", true, "2", "kmsvc.orders.fifo.shard-2"},
|
||
|
|
}
|
||
|
|
for _, c := range cases {
|
||
|
|
t.Run(c.name, func(t *testing.T) {
|
||
|
|
if got := ShardTopicName(c.queue, c.fifo, c.shard); got != c.want {
|
||
|
|
t.Errorf("ShardTopicName(%q, %v, %q) = %q, want %q", c.queue, c.fifo, c.shard, got, c.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDLQShardTopicName(t *testing.T) {
|
||
|
|
if got, want := DLQShardTopicName("orders", false, "0"), "kmsvc.orders.dlq.shard-0"; got != want {
|
||
|
|
t.Errorf("DLQShardTopicName = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
if got, want := DLQShardTopicName("orders", true, "0"), "kmsvc.orders.fifo.dlq.shard-0"; got != want {
|
||
|
|
t.Errorf("DLQShardTopicName(fifo) = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPartitionWithinShardDeterministic(t *testing.T) {
|
||
|
|
const partitions = 6
|
||
|
|
p1 := PartitionWithinShard("group-a", partitions)
|
||
|
|
p2 := PartitionWithinShard("group-a", partitions)
|
||
|
|
if p1 != p2 {
|
||
|
|
t.Errorf("PartitionWithinShard not deterministic: %d != %d", p1, p2)
|
||
|
|
}
|
||
|
|
if p1 < 0 || p1 >= partitions {
|
||
|
|
t.Errorf("PartitionWithinShard out of range: %d", p1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPartitionWithinShardDistribution(t *testing.T) {
|
||
|
|
const partitions = 6
|
||
|
|
seen := map[int32]bool{}
|
||
|
|
for i := 0; i < 1000; i++ {
|
||
|
|
groupID := "group-" + string(rune('a'+i%26)) + string(rune('0'+i%10))
|
||
|
|
p := PartitionWithinShard(groupID, partitions)
|
||
|
|
seen[p] = true
|
||
|
|
}
|
||
|
|
if len(seen) < 2 {
|
||
|
|
t.Errorf("expected groups to spread across multiple partitions, got %d distinct partitions", len(seen))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSelectShard(t *testing.T) {
|
||
|
|
mid := SplitHashRange(0, FullHashRangeEnd)
|
||
|
|
shards := []Shard{
|
||
|
|
{ID: "1", Topic: "kmsvc.orders.fifo.shard-1", HashRangeStart: 0, HashRangeEnd: mid},
|
||
|
|
{ID: "2", Topic: "kmsvc.orders.fifo.shard-2", HashRangeStart: mid, HashRangeEnd: FullHashRangeEnd},
|
||
|
|
}
|
||
|
|
for i := 0; i < 1000; i++ {
|
||
|
|
key := "group-" + string(rune('a'+i%26)) + string(rune('0'+i%10))
|
||
|
|
s, ok := SelectShard(shards, key)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("no shard found for key %q", key)
|
||
|
|
}
|
||
|
|
h := HashKey(key)
|
||
|
|
if h < s.HashRangeStart || h >= s.HashRangeEnd {
|
||
|
|
t.Errorf("key %q hash %d outside selected shard range [%d, %d)", key, h, s.HashRangeStart, s.HashRangeEnd)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSelectShardStableAcrossSplit(t *testing.T) {
|
||
|
|
parent := []Shard{{ID: "0", Topic: "kmsvc.orders.fifo.shard-0", HashRangeStart: 0, HashRangeEnd: FullHashRangeEnd}}
|
||
|
|
mid := SplitHashRange(0, FullHashRangeEnd)
|
||
|
|
children := []Shard{
|
||
|
|
{ID: "1", Topic: "kmsvc.orders.fifo.shard-1", HashRangeStart: 0, HashRangeEnd: mid},
|
||
|
|
{ID: "2", Topic: "kmsvc.orders.fifo.shard-2", HashRangeStart: mid, HashRangeEnd: FullHashRangeEnd},
|
||
|
|
}
|
||
|
|
for i := 0; i < 1000; i++ {
|
||
|
|
key := "group-" + string(rune('a'+i%26)) + string(rune('0'+i%10))
|
||
|
|
before, _ := SelectShard(parent, key)
|
||
|
|
after, ok := SelectShard(children, key)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("no child shard found for key %q", key)
|
||
|
|
}
|
||
|
|
h := HashKey(key)
|
||
|
|
wantParent := h < mid
|
||
|
|
gotParentSide := after.ID == "1"
|
||
|
|
if wantParent != gotParentSide {
|
||
|
|
t.Errorf("key %q (hash %d) routed to wrong child after split", key, h)
|
||
|
|
}
|
||
|
|
_ = before
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateNoDLQCycle(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
queue string
|
||
|
|
isDLQ bool
|
||
|
|
target string
|
||
|
|
expectErr bool
|
||
|
|
}{
|
||
|
|
{"no target", "orders", false, "", false},
|
||
|
|
{"valid target", "orders", false, "orders-dlq", false},
|
||
|
|
{"self reference", "orders", false, "orders", true},
|
||
|
|
{"dlq with target", "orders-dlq", true, "another-dlq", true},
|
||
|
|
}
|
||
|
|
for _, c := range cases {
|
||
|
|
t.Run(c.name, func(t *testing.T) {
|
||
|
|
err := ValidateNoDLQCycle(c.queue, c.isDLQ, c.target)
|
||
|
|
if c.expectErr && err == nil {
|
||
|
|
t.Errorf("expected error, got nil")
|
||
|
|
}
|
||
|
|
if !c.expectErr && err != nil {
|
||
|
|
t.Errorf("expected no error, got %v", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|