单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。 Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
3.3 KiB
Go
134 lines
3.3 KiB
Go
// selector_test.go:P2C + sticky LRU 单测(无外网,固定随机源)。
|
||
package proxymanager
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// fixedRnd 固定序列随机源(可复现)。
|
||
func fixedRnd(seq []int) func(int) int {
|
||
i := 0
|
||
return func(n int) int {
|
||
v := seq[i%len(seq)] % n
|
||
i++
|
||
return v
|
||
}
|
||
}
|
||
|
||
func TestPickLowestDelay(t *testing.T) {
|
||
cands := []string{"A", "B", "C"}
|
||
delays := map[string]int{"A": 200, "B": 100, "C": 300}
|
||
picked := Pick(cands, func(n string) int { return delays[n] }, fixedRnd([]int{0, 1}))
|
||
if picked != "B" {
|
||
t.Errorf("P2C picked = %s, want B(最低延迟)", picked)
|
||
}
|
||
}
|
||
|
||
func TestPickUnknownDelayNotPreferred(t *testing.T) {
|
||
// 未探测节点(<=0)视为最差:A 已探测 500ms,B 未探测 → 应选 A
|
||
cands := []string{"A", "B"}
|
||
picked := Pick(cands, func(n string) int {
|
||
if n == "A" {
|
||
return 500
|
||
}
|
||
return 0
|
||
}, fixedRnd([]int{0, 1}))
|
||
if picked != "A" {
|
||
t.Errorf("picked = %s, want A(未探测节点不应霸占选择)", picked)
|
||
}
|
||
}
|
||
|
||
func TestPickSingleCandidate(t *testing.T) {
|
||
if got := Pick([]string{"only"}, func(string) int { return 1 }, fixedRnd([]int{0})); got != "only" {
|
||
t.Errorf("单候选 picked = %s", got)
|
||
}
|
||
if got := Pick(nil, func(string) int { return 0 }, fixedRnd([]int{0})); got != "" {
|
||
t.Errorf("空候选 picked = %q, want 空串", got)
|
||
}
|
||
}
|
||
|
||
func TestPickNeverSameIndexTwice(t *testing.T) {
|
||
// rnd 恒返同值时,两样本必须不同(j=(i+1)%n 兜底)
|
||
cands := []string{"A", "B", "C"}
|
||
delays := map[string]int{"A": 100, "B": 200, "C": 300}
|
||
picked := Pick(cands, func(n string) int { return delays[n] }, func(int) int { return 0 })
|
||
if picked != "A" {
|
||
t.Errorf("picked = %s, want A(0 与 1 比较取低)", picked)
|
||
}
|
||
}
|
||
|
||
func TestStickyTTLExpiry(t *testing.T) {
|
||
s := NewStickyTable(time.Minute, 10)
|
||
s.Set("k", "node1")
|
||
if n, ok := s.Get("k"); !ok || n != "node1" {
|
||
t.Fatalf("Get = %q,%v", n, ok)
|
||
}
|
||
// TTL 过期(直接改 ExpireAt 模拟时钟前进)
|
||
s.mu.Lock()
|
||
e := s.m["k"]
|
||
e.ExpireAt = time.Now().Add(-time.Second)
|
||
s.m["k"] = e
|
||
s.mu.Unlock()
|
||
if _, ok := s.Get("k"); ok {
|
||
t.Fatal("过期条目应失效")
|
||
}
|
||
}
|
||
|
||
func TestStickyLRUEviction(t *testing.T) {
|
||
s := NewStickyTable(time.Hour, 3)
|
||
s.Set("a", "1")
|
||
s.Set("b", "2")
|
||
s.Set("c", "3")
|
||
s.Get("a") // touch a → b 变最旧
|
||
s.Set("d", "4")
|
||
if _, ok := s.Get("b"); ok {
|
||
t.Fatal("LRU 应淘汰 b(容量 3)")
|
||
}
|
||
if _, ok := s.Get("a"); !ok {
|
||
t.Fatal("a 被访问过不应被淘汰")
|
||
}
|
||
if s.Len() != 3 {
|
||
t.Errorf("Len = %d, want 3", s.Len())
|
||
}
|
||
}
|
||
|
||
func TestStickyReset(t *testing.T) {
|
||
s := NewStickyTable(time.Hour, 10)
|
||
s.Set("k", "v")
|
||
s.Reset()
|
||
if s.Len() != 0 {
|
||
t.Fatal("Reset 后应为空")
|
||
}
|
||
}
|
||
|
||
func TestStickyKey(t *testing.T) {
|
||
if StickyKey("sess1", "d.com") != "s:sess1" {
|
||
t.Error("session 优先")
|
||
}
|
||
if StickyKey("", "d.com") != "d:d.com" {
|
||
t.Error("无 session 用 domain")
|
||
}
|
||
}
|
||
|
||
func TestStickyDefaultCapacity(t *testing.T) {
|
||
s := NewStickyTable(0, 0) // 走默认值:TTL 10min / 容量 1000
|
||
for i := 0; i < 1200; i++ {
|
||
s.Set(string(rune('a'+i%26))+itoa(i), "n")
|
||
}
|
||
if s.Len() > 1000 {
|
||
t.Errorf("容量上限失效: Len=%d", s.Len())
|
||
}
|
||
}
|
||
|
||
func itoa(n int) string {
|
||
if n == 0 {
|
||
return "0"
|
||
}
|
||
var b []byte
|
||
for n > 0 {
|
||
b = append([]byte{byte('0' + n%10)}, b...)
|
||
n /= 10
|
||
}
|
||
return string(b)
|
||
}
|