lolly/internal/loadbalance/balancer_test.go
xfy 4de32812f2 perf(loadbalance): 预计算一致性哈希虚拟节点哈希值
- Target 结构新增 VirtualHashes 字段存储预计算哈希
- 新增 PrecomputeHashes 方法在初始化时计算虚拟节点哈希
- SelectExcludingByKey 使用预计算哈希避免运行时重复计算
- 新增 SelectExcludingByKey 测试和基准测试

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:03:10 +08:00

936 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package loadbalance 提供负载均衡算法的测试。
//
// 该文件测试负载均衡模块的各项功能,包括:
// - 轮询算法
// - 加权轮询算法
// - 最少连接算法
// - IP 哈希算法
// - 一致性哈希算法
//
// 作者xfy
package loadbalance
import (
"sync"
"testing"
)
// createHealthyTarget 创建一个带有健康状态的目标(辅助函数)
func createHealthyTarget(url string, healthy bool) *Target {
t := &Target{URL: url}
t.Healthy.Store(healthy)
return t
}
// TestRoundRobin_Select 测试轮询负载均衡选择器。
func TestRoundRobin_Select(t *testing.T) {
t.Run("多目标轮询", func(t *testing.T) {
rr := NewRoundRobin()
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
// 验证轮询顺序
got1 := rr.Select(targets)
got2 := rr.Select(targets)
got3 := rr.Select(targets)
got4 := rr.Select(targets)
if got1.URL != "http://backend1:8080" {
t.Errorf("第一次选择 = %q, want %q", got1.URL, "http://backend1:8080")
}
if got2.URL != "http://backend2:8080" {
t.Errorf("第二次选择 = %q, want %q", got2.URL, "http://backend2:8080")
}
if got3.URL != "http://backend3:8080" {
t.Errorf("第三次选择 = %q, want %q", got3.URL, "http://backend3:8080")
}
if got4.URL != "http://backend1:8080" {
t.Errorf("第四次选择 = %q, want %q", got4.URL, "http://backend1:8080")
}
})
t.Run("单目标", func(t *testing.T) {
rr := NewRoundRobin()
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
}
got := rr.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend1:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend1:8080")
}
})
t.Run("空目标", func(t *testing.T) {
rr := NewRoundRobin()
got := rr.Select([]*Target{})
if got != nil {
t.Errorf("Select() = %v, want nil", got)
}
})
t.Run("跳过不健康目标", func(t *testing.T) {
rr := NewRoundRobin()
targets := []*Target{
createHealthyTarget("http://backend1:8080", false),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", false),
}
got := rr.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend2:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend2:8080")
}
})
t.Run("所有目标都不健康", func(t *testing.T) {
rr := NewRoundRobin()
targets := []*Target{
createHealthyTarget("http://backend1:8080", false),
createHealthyTarget("http://backend2:8080", false),
}
got := rr.Select(targets)
if got != nil {
t.Errorf("Select() = %v, want nil", got)
}
})
t.Run("并发安全", func(t *testing.T) {
rr := NewRoundRobin()
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = rr.Select(targets)
}()
}
wg.Wait()
})
}
// TestWeightedRoundRobin_Select 测试加权轮询负载均衡选择器。
func TestWeightedRoundRobin_Select(t *testing.T) {
t.Run("权重分配", func(t *testing.T) {
wrr := NewWeightedRoundRobin()
targets := []*Target{
{URL: "http://backend1:8080", Weight: 1},
{URL: "http://backend2:8080", Weight: 3},
}
targets[0].Healthy.Store(true)
targets[1].Healthy.Store(true)
// 统计选择次数
counts := make(map[string]int)
for i := 0; i < 400; i++ {
got := wrr.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
counts[got.URL]++
}
// 权重1:3期望比例大约为1:3
// 允许一定误差
ratio := float64(counts["http://backend2:8080"]) / float64(counts["http://backend1:8080"])
if ratio < 2.0 || ratio > 4.0 {
t.Errorf("权重比例 = %f, 期望接近 3.0", ratio)
}
})
t.Run("权重为0", func(t *testing.T) {
wrr := NewWeightedRoundRobin()
targets := []*Target{
{URL: "http://backend1:8080", Weight: 0},
{URL: "http://backend2:8080", Weight: 1},
}
targets[0].Healthy.Store(true)
targets[1].Healthy.Store(true)
// 权重为0的目标应该被当作权重为1处理
counts := make(map[string]int)
for i := 0; i < 100; i++ {
got := wrr.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
counts[got.URL]++
}
// 两个目标都应该被选中
if counts["http://backend1:8080"] == 0 {
t.Error("权重为0的目标从未被选中")
}
if counts["http://backend2:8080"] == 0 {
t.Error("权重为1的目标从未被选中")
}
})
t.Run("空目标", func(t *testing.T) {
wrr := NewWeightedRoundRobin()
got := wrr.Select([]*Target{})
if got != nil {
t.Errorf("Select() = %v, want nil", got)
}
})
t.Run("所有目标权重为0或不健康", func(t *testing.T) {
wrr := NewWeightedRoundRobin()
targets := []*Target{
{URL: "http://backend1:8080", Weight: 0},
{URL: "http://backend2:8080", Weight: 0},
}
targets[0].Healthy.Store(false)
targets[1].Healthy.Store(false)
got := wrr.Select(targets)
if got != nil {
t.Errorf("Select() = %v, want nil", got)
}
})
t.Run("跳过不健康目标", func(t *testing.T) {
wrr := NewWeightedRoundRobin()
targets := []*Target{
{URL: "http://backend1:8080", Weight: 5},
{URL: "http://backend2:8080", Weight: 1},
}
targets[0].Healthy.Store(false)
targets[1].Healthy.Store(true)
// 所有选择都应该落在健康目标上
for i := 0; i < 50; i++ {
got := wrr.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend2:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend2:8080")
}
}
})
}
// TestLeastConnections_Select 测试最少连接负载均衡选择器。
func TestLeastConnections_Select(t *testing.T) {
t.Run("选择最少连接", func(t *testing.T) {
lc := NewLeastConnections()
target1 := &Target{URL: "http://backend1:8080", Connections: 10}
target1.Healthy.Store(true)
target2 := &Target{URL: "http://backend2:8080", Connections: 5}
target2.Healthy.Store(true)
target3 := &Target{URL: "http://backend3:8080", Connections: 15}
target3.Healthy.Store(true)
targets := []*Target{target1, target2, target3}
got := lc.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend2:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend2:8080")
}
})
t.Run("连接数相等时选择第一个", func(t *testing.T) {
lc := NewLeastConnections()
targets := []*Target{
{URL: "http://backend1:8080", Connections: 5},
{URL: "http://backend2:8080", Connections: 5},
}
targets[0].Healthy.Store(true)
targets[1].Healthy.Store(true)
got := lc.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend1:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend1:8080")
}
})
t.Run("空目标", func(t *testing.T) {
lc := NewLeastConnections()
got := lc.Select([]*Target{})
if got != nil {
t.Errorf("Select() = %v, want nil", got)
}
})
t.Run("跳过不健康目标", func(t *testing.T) {
lc := NewLeastConnections()
targets := []*Target{
{URL: "http://backend1:8080", Connections: 1},
{URL: "http://backend2:8080", Connections: 10},
}
targets[0].Healthy.Store(false)
targets[1].Healthy.Store(true)
got := lc.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend2:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend2:8080")
}
})
t.Run("所有目标都不健康", func(t *testing.T) {
lc := NewLeastConnections()
targets := []*Target{
{URL: "http://backend1:8080", Connections: 1},
{URL: "http://backend2:8080", Connections: 2},
}
targets[0].Healthy.Store(false)
targets[1].Healthy.Store(false)
got := lc.Select(targets)
if got != nil {
t.Errorf("Select() = %v, want nil", got)
}
})
}
// TestIPHash_Select 测试IP哈希负载均衡选择器。
func TestIPHash_Select(t *testing.T) {
t.Run("相同IP返回相同目标", func(t *testing.T) {
ih := NewIPHash()
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
// 使用相同的IP地址多次选择
clientIP := "192.168.1.100"
var firstSelection *Target
for i := 0; i < 10; i++ {
got := ih.SelectByIP(targets, clientIP)
if got == nil {
t.Fatal("SelectByIP() = nil, want non-nil")
}
if firstSelection == nil {
firstSelection = got
} else if got.URL != firstSelection.URL {
t.Errorf("相同IP选择不同目标: 第一次=%q, 后续=%q", firstSelection.URL, got.URL)
}
}
})
t.Run("不同IP分配", func(t *testing.T) {
ih := NewIPHash()
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
}
// 使用不同的IP地址
ips := []string{"192.168.1.1", "192.168.1.2", "10.0.0.1", "10.0.0.2"}
selections := make(map[string]string)
for _, ip := range ips {
got := ih.SelectByIP(targets, ip)
if got == nil {
t.Fatal("SelectByIP() = nil, want non-nil")
}
selections[ip] = got.URL
}
// 验证每个IP都有分配不验证具体分配到哪个
for _, ip := range ips {
if selections[ip] == "" {
t.Errorf("IP %s 没有分配到目标", ip)
}
}
})
t.Run("空目标", func(t *testing.T) {
ih := NewIPHash()
got := ih.SelectByIP([]*Target{}, "192.168.1.1")
if got != nil {
t.Errorf("SelectByIP() = %v, want nil", got)
}
})
t.Run("Select方法使用空IP", func(t *testing.T) {
ih := NewIPHash()
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
}
got := ih.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend1:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend1:8080")
}
})
t.Run("跳过不健康目标", func(t *testing.T) {
ih := NewIPHash()
targets := []*Target{
createHealthyTarget("http://backend1:8080", false),
createHealthyTarget("http://backend2:8080", true),
}
got := ih.SelectByIP(targets, "192.168.1.1")
if got == nil {
t.Fatal("SelectByIP() = nil, want non-nil")
}
if got.URL != "http://backend2:8080" {
t.Errorf("SelectByIP() = %q, want %q", got.URL, "http://backend2:8080")
}
})
}
// TestConnectionsAtomic 测试连接数的原子操作。
func TestConnectionsAtomic(t *testing.T) {
t.Run("IncrementConnections", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080", Connections: 0}
target.Healthy.Store(true)
IncrementConnections(target)
if target.Connections != 1 {
t.Errorf("Connections = %d, want 1", target.Connections)
}
IncrementConnections(target)
if target.Connections != 2 {
t.Errorf("Connections = %d, want 2", target.Connections)
}
})
t.Run("DecrementConnections", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080", Connections: 5}
target.Healthy.Store(true)
DecrementConnections(target)
if target.Connections != 4 {
t.Errorf("Connections = %d, want 4", target.Connections)
}
DecrementConnections(target)
if target.Connections != 3 {
t.Errorf("Connections = %d, want 3", target.Connections)
}
})
t.Run("并发IncrementConnections", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080", Connections: 0}
target.Healthy.Store(true)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
IncrementConnections(target)
}()
}
wg.Wait()
if target.Connections != 1000 {
t.Errorf("Connections = %d, want 1000", target.Connections)
}
})
t.Run("并发DecrementConnections", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080", Connections: 1000}
target.Healthy.Store(true)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
DecrementConnections(target)
}()
}
wg.Wait()
if target.Connections != 0 {
t.Errorf("Connections = %d, want 0", target.Connections)
}
})
t.Run("混合增减操作", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080", Connections: 100}
target.Healthy.Store(true)
var wg sync.WaitGroup
// 500个增加
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
IncrementConnections(target)
}()
}
// 300个减少
for i := 0; i < 300; i++ {
wg.Add(1)
go func() {
defer wg.Done()
DecrementConnections(target)
}()
}
wg.Wait()
// 100 + 500 - 300 = 300
if target.Connections != 300 {
t.Errorf("Connections = %d, want 300", target.Connections)
}
})
t.Run("允许负值", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080", Connections: 0}
target.Healthy.Store(true)
DecrementConnections(target)
if target.Connections != -1 {
t.Errorf("Connections = %d, want -1", target.Connections)
}
})
}
// TestHealthStatus 测试健康状态操作。
func TestHealthStatus(t *testing.T) {
t.Run("IsHealthy", func(t *testing.T) {
tests := []struct {
name string
target *Target
want bool
}{
{
name: "健康目标",
target: createHealthyTarget("http://backend1:8080", true),
want: true,
},
{
name: "不健康目标",
target: createHealthyTarget("http://backend1:8080", false),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.target.Healthy.Load()
if got != tt.want {
t.Errorf("Healthy.Load() = %v, want %v", got, tt.want)
}
})
}
})
t.Run("SetHealthy", func(t *testing.T) {
target := &Target{URL: "http://backend1:8080"}
target.Healthy.Store(true)
// 设置为不健康
target.Healthy.Store(false)
if target.Healthy.Load() {
t.Error("Store(false) 后期望 Load = false, 但 got true")
}
// 设置为健康
target.Healthy.Store(true)
if !target.Healthy.Load() {
t.Error("Store(true) 后期望 Load = true, 但 got false")
}
})
}
// TestFilterHealthy 测试filterHealthy辅助函数。
func TestFilterHealthy(t *testing.T) {
t.Run("过滤健康目标", func(t *testing.T) {
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", false),
createHealthyTarget("http://backend3:8080", true),
createHealthyTarget("http://backend4:8080", false),
}
got := filterHealthy(targets)
if len(got) != 2 {
t.Errorf("len(filterHealthy) = %d, want 2", len(got))
}
// 验证返回的都是健康目标
for _, target := range got {
if !target.Healthy.Load() {
t.Errorf("filterHealthy 返回了不健康目标: %q", target.URL)
}
}
})
t.Run("全部健康", func(t *testing.T) {
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
}
got := filterHealthy(targets)
if len(got) != 2 {
t.Errorf("len(filterHealthy) = %d, want 2", len(got))
}
})
t.Run("全部不健康", func(t *testing.T) {
targets := []*Target{
createHealthyTarget("http://backend1:8080", false),
createHealthyTarget("http://backend2:8080", false),
}
got := filterHealthy(targets)
if len(got) != 0 {
t.Errorf("len(filterHealthy) = %d, want 0", len(got))
}
})
t.Run("空切片", func(t *testing.T) {
got := filterHealthy([]*Target{})
if len(got) != 0 {
t.Errorf("len(filterHealthy) = %d, want 0", len(got))
}
})
t.Run("nil切片", func(t *testing.T) {
got := filterHealthy(nil)
if len(got) != 0 {
t.Errorf("len(filterHealthy) = %d, want 0", len(got))
}
})
}
// TestBalancerInterface 测试各种负载均衡器都实现了Balancer接口。
func TestBalancerInterface(t *testing.T) {
tests := []struct {
name string
balancer Balancer
}{
{
name: "RoundRobin",
balancer: NewRoundRobin(),
},
{
name: "WeightedRoundRobin",
balancer: NewWeightedRoundRobin(),
},
{
name: "LeastConnections",
balancer: NewLeastConnections(),
},
{
name: "IPHash",
balancer: NewIPHash(),
},
}
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.balancer.Select(targets)
if got == nil {
t.Fatal("Select() = nil, want non-nil")
}
if got.URL != "http://backend1:8080" {
t.Errorf("Select() = %q, want %q", got.URL, "http://backend1:8080")
}
})
}
}
// TestConsistentHash 测试一致性哈希负载均衡器。
func TestConsistentHash(t *testing.T) {
t.Run("创建默认配置", func(t *testing.T) {
ch := NewConsistentHash(0, "ip")
if ch == nil {
t.Fatal("NewConsistentHash() = nil")
}
if ch.GetVirtualNodes() != 150 {
t.Errorf("GetVirtualNodes() = %d, want 150", ch.GetVirtualNodes())
}
if ch.GetHashKey() != "ip" {
t.Errorf("GetHashKey() = %q, want %q", ch.GetHashKey(), "ip")
}
})
t.Run("自定义虚拟节点数", func(t *testing.T) {
ch := NewConsistentHash(200, "uri")
if ch.GetVirtualNodes() != 200 {
t.Errorf("GetVirtualNodes() = %d, want 200", ch.GetVirtualNodes())
}
})
t.Run("SelectByKey 空目标", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
got := ch.SelectByKey([]*Target{}, "192.168.1.1")
if got != nil {
t.Errorf("SelectByKey() = %v, want nil", got)
}
})
t.Run("SelectByKey 单目标", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
}
got := ch.SelectByKey(targets, "192.168.1.1")
if got == nil {
t.Fatal("SelectByKey() = nil")
}
if got.URL != "http://backend1:8080" {
t.Errorf("SelectByKey() = %q, want %q", got.URL, "http://backend1:8080")
}
})
t.Run("SelectByKey 多目标相同键", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
// 相同的键应该选择相同的目标
key := "192.168.1.100"
first := ch.SelectByKey(targets, key)
for i := 0; i < 10; i++ {
got := ch.SelectByKey(targets, key)
if got == nil {
t.Fatal("SelectByKey() = nil")
}
if got.URL != first.URL {
t.Errorf("相同键选择不同目标: first=%q, got=%q", first.URL, got.URL)
}
}
})
t.Run("GetStats", func(t *testing.T) {
ch := NewConsistentHash(100, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
}
ch.Rebuild(targets)
stats := ch.GetStats()
if stats.VirtualNodes != 100 {
t.Errorf("VirtualNodes = %d, want 100", stats.VirtualNodes)
}
if stats.CircleSize != 200 { // 2 targets * 100 nodes
t.Errorf("CircleSize = %d, want 200", stats.CircleSize)
}
})
t.Run("Rebuild 跳过不健康目标", func(t *testing.T) {
ch := NewConsistentHash(10, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", false),
createHealthyTarget("http://backend2:8080", true),
}
ch.Rebuild(targets)
stats := ch.GetStats()
if stats.CircleSize != 10 { // 只有1个健康目标 * 10 nodes
t.Errorf("CircleSize = %d, want 10", stats.CircleSize)
}
})
}
// TestConsistentHashSelectExcludingByKey 测试一致性哈希排除选择功能。
func TestConsistentHashSelectExcludingByKey(t *testing.T) {
t.Run("空排除列表", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
ch.Rebuild(targets)
key := "192.168.1.100"
got := ch.SelectExcludingByKey(targets, []*Target{}, key)
if got == nil {
t.Fatal("SelectExcludingByKey() = nil, want non-nil")
}
// 验证正常选择行为
got2 := ch.SelectExcludingByKey(targets, nil, key)
if got2 == nil {
t.Fatal("SelectExcludingByKey() with nil = nil, want non-nil")
}
if got.URL != got2.URL {
t.Errorf("空排除和nil排除应该返回相同结果: empty=%q, nil=%q", got.URL, got2.URL)
}
})
t.Run("部分排除", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
ch.Rebuild(targets)
// 排除第一个目标
excluded := []*Target{targets[0]}
key := "192.168.1.100"
// 多次选择,验证不会选中排除的目标
for i := 0; i < 100; i++ {
got := ch.SelectExcludingByKey(targets, excluded, key)
if got == nil {
t.Fatal("SelectExcludingByKey() = nil, want non-nil")
}
if got.URL == targets[0].URL {
t.Errorf("选中了被排除的目标: %q", got.URL)
}
}
})
t.Run("全部排除", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
}
ch.Rebuild(targets)
// 排除所有目标
excluded := []*Target{targets[0], targets[1]}
key := "192.168.1.100"
got := ch.SelectExcludingByKey(targets, excluded, key)
if got != nil {
t.Errorf("SelectExcludingByKey() = %q, want nil (all excluded)", got.URL)
}
})
t.Run("排除包含nil目标", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
}
ch.Rebuild(targets)
// 排除列表中包含nil
excluded := []*Target{nil, targets[0]}
key := "192.168.1.100"
got := ch.SelectExcludingByKey(targets, excluded, key)
if got == nil {
t.Fatal("SelectExcludingByKey() = nil, want non-nil")
}
if got.URL == targets[0].URL {
t.Errorf("选中了被排除的目标: %q", got.URL)
}
})
t.Run("并发安全", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
ch.Rebuild(targets)
excluded := []*Target{targets[0]}
key := "192.168.1.100"
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
got := ch.SelectExcludingByKey(targets, excluded, key)
if got != nil && got.URL == targets[0].URL {
t.Errorf("并发时选中了被排除的目标: %q", got.URL)
}
}
}()
}
wg.Wait()
})
t.Run("相同键一致性", func(t *testing.T) {
ch := NewConsistentHash(150, "ip")
targets := []*Target{
createHealthyTarget("http://backend1:8080", true),
createHealthyTarget("http://backend2:8080", true),
createHealthyTarget("http://backend3:8080", true),
}
ch.Rebuild(targets)
excluded := []*Target{targets[0]}
key := "192.168.1.100"
// 相同键应该始终返回相同的目标
var firstSelection *Target
for i := 0; i < 50; i++ {
got := ch.SelectExcludingByKey(targets, excluded, key)
if got == nil {
t.Fatal("SelectExcludingByKey() = nil, want non-nil")
}
if firstSelection == nil {
firstSelection = got
} else if got.URL != firstSelection.URL {
t.Errorf("相同键选择不同目标: first=%q, got=%q", firstSelection.URL, got.URL)
}
}
})
}
func TestIsValidAlgorithm(t *testing.T) {
tests := []struct {
name string
algorithm string
want bool
}{
{"round_robin", "round_robin", true},
{"weighted_round_robin", "weighted_round_robin", true},
{"least_conn", "least_conn", true},
{"ip_hash", "ip_hash", true},
{"consistent_hash", "consistent_hash", true},
{"invalid", "invalid", false},
{"empty", "", true}, // 空字符串有效(使用默认值)
{"unknown", "unknown-algorithm", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsValidAlgorithm(tt.algorithm)
if got != tt.want {
t.Errorf("IsValidAlgorithm(%q) = %v, want %v", tt.algorithm, got, tt.want)
}
})
}
}