Batch 1 - Crash/data corruption: - Add nil/empty config guards in server and app - Protect against fasthttp buffer mutation in logging - Ensure mimeutil falls back before caching empty MIME - Make proxy health checker and resolver tolerate nil cfg - Prevent Lua EMERG/ALERT/CRIT log levels from killing process Batch 2 - Concurrency/race conditions: - Add focused race tests for compression pool, ssl/ocsp, server proxies slice, Lua timer cancel, and TCP socket connect - Fix Lua timer active race and socket ConnectAsync race Batch 3 - Resource leaks/functional damage: - Track and stop rate limiter cleanup goroutines on shutdown - Add tests for proxy connection count leaks, server pool deadlock, Linux sendfile integrity, WebSocket frame data loss, stream UDP stop deadlock, and upstream name mismatch - Fix stream ListenTCP/upstream lookup and UDP shutdown Batch 4 - High severity technical debt: - Add nil guard in security headers middleware - Validate sliding window divisor and add tests - Protect static handler fields with existing RWMutex - Avoid mutating live HostClient.Addr on DNS updates - Fix slow start Start/Stop and resolver restart - Initialize variable fallback context maps - Add tests for request_id/time_local and logging file handle close Also: - Relax integration variable performance threshold under race detector - Update benchmark plan docs for new stream ListenTCP signature Verification: - make test: pass - go test -race -count=1 ./internal/...: pass - make lint: 0 issues - make build: success
354 lines
8.8 KiB
Go
354 lines
8.8 KiB
Go
// Package proxy 提供健康检查功能的测试。
|
||
//
|
||
// 该文件测试健康检查模块的各项功能,包括:
|
||
// - 健康检查器创建
|
||
// - 默认值应用
|
||
// - 自定义配置
|
||
// - 负值配置处理
|
||
// - 零值配置处理
|
||
// - 启动和停止控制
|
||
// - 目标健康检查
|
||
// - 超时处理
|
||
// - 连接失败处理
|
||
// - 标记不健康
|
||
//
|
||
// 作者:xfy
|
||
package proxy
|
||
|
||
import (
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/loadbalance"
|
||
)
|
||
|
||
// TestCheckTarget 测试 checkTarget 方法。
|
||
func TestCheckTarget(t *testing.T) {
|
||
t.Run("健康响应", func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path != healthPath {
|
||
t.Errorf("请求路径 = %q, want %q", r.URL.Path, healthPath)
|
||
}
|
||
w.WriteHeader(http.StatusOK)
|
||
}))
|
||
defer server.Close()
|
||
|
||
target := &loadbalance.Target{
|
||
URL: server.URL,
|
||
}
|
||
target.Healthy.Store(false)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if !target.Healthy.Load() {
|
||
t.Error("健康响应后 target 应标记为 healthy")
|
||
}
|
||
})
|
||
|
||
t.Run("不健康响应", func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
}))
|
||
defer server.Close()
|
||
|
||
target := &loadbalance.Target{
|
||
URL: server.URL,
|
||
}
|
||
target.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("5xx 响应后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("超时", func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||
time.Sleep(100 * time.Millisecond)
|
||
}))
|
||
defer server.Close()
|
||
|
||
target := &loadbalance.Target{
|
||
URL: server.URL,
|
||
}
|
||
target.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 10 * time.Millisecond,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("超时后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("连接失败", func(t *testing.T) {
|
||
target := &loadbalance.Target{
|
||
URL: "http://invalid-host-that-does-not-exist:99999",
|
||
}
|
||
target.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 100 * time.Millisecond,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("连接失败后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("3xx 重定向响应", func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
w.WriteHeader(http.StatusMovedPermanently)
|
||
}))
|
||
defer server.Close()
|
||
|
||
target := &loadbalance.Target{
|
||
URL: server.URL,
|
||
}
|
||
target.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("3xx 响应后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("4xx 客户端错误响应", func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
w.WriteHeader(http.StatusNotFound)
|
||
}))
|
||
defer server.Close()
|
||
|
||
target := &loadbalance.Target{
|
||
URL: server.URL,
|
||
}
|
||
target.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("4xx 响应后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("2xx 成功响应", func(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
statusCode int
|
||
}{
|
||
{"200 OK", http.StatusOK},
|
||
{"201 Created", http.StatusCreated},
|
||
{"204 No Content", http.StatusNoContent},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
w.WriteHeader(tt.statusCode)
|
||
}))
|
||
defer server.Close()
|
||
|
||
target := &loadbalance.Target{
|
||
URL: server.URL,
|
||
}
|
||
target.Healthy.Store(false)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.checkTarget(target)
|
||
|
||
if !target.Healthy.Load() {
|
||
t.Errorf("%d 响应后 target 应标记为 healthy", tt.statusCode)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestNewHealthChecker_NilConfig 验证 nil 健康检查配置不会 panic。
|
||
func TestNewHealthChecker_NilConfig(t *testing.T) {
|
||
target := &loadbalance.Target{URL: "http://127.0.0.1:8080"}
|
||
|
||
// 不应 panic
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, nil)
|
||
if checker == nil {
|
||
t.Fatal("NewHealthChecker(nil) should return non-nil checker")
|
||
}
|
||
|
||
if checker.interval != 10*time.Second {
|
||
t.Errorf("expected default interval 10s, got %v", checker.interval)
|
||
}
|
||
if checker.timeout != 5*time.Second {
|
||
t.Errorf("expected default timeout 5s, got %v", checker.timeout)
|
||
}
|
||
if checker.path != healthPath {
|
||
t.Errorf("expected default path %q, got %q", healthPath, checker.path)
|
||
}
|
||
}
|
||
|
||
// TestMarkUnhealthy 测试 MarkUnhealthy 方法。
|
||
func TestMarkUnhealthy(t *testing.T) {
|
||
t.Run("标记不健康", func(t *testing.T) {
|
||
target := &loadbalance.Target{
|
||
URL: "http://backend1:8080",
|
||
}
|
||
target.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.MarkUnhealthy(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("MarkUnhealthy 后 target 应标记为 unhealthy")
|
||
}
|
||
})
|
||
|
||
t.Run("已不健康的 target 再次标记", func(t *testing.T) {
|
||
target := &loadbalance.Target{
|
||
URL: "http://backend1:8080",
|
||
}
|
||
target.Healthy.Store(false)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.MarkUnhealthy(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("MarkUnhealthy 后 target 应保持 unhealthy 状态")
|
||
}
|
||
})
|
||
|
||
t.Run("多 target 场景", func(t *testing.T) {
|
||
target1 := &loadbalance.Target{
|
||
URL: "http://backend1:8080",
|
||
}
|
||
target1.Healthy.Store(true)
|
||
target2 := &loadbalance.Target{
|
||
URL: "http://backend2:8080",
|
||
}
|
||
target2.Healthy.Store(true)
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target1, target2}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: healthPath,
|
||
})
|
||
|
||
checker.MarkUnhealthy(target1)
|
||
|
||
if target1.Healthy.Load() {
|
||
t.Error("target1 应标记为 unhealthy")
|
||
}
|
||
if !target2.Healthy.Load() {
|
||
t.Error("target2 应保持 healthy")
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestMarkUnhealthy_WithSlowStartManager 测试 MarkUnhealthy 与 SlowStartManager 集成。
|
||
func TestMarkUnhealthy_WithSlowStartManager(t *testing.T) {
|
||
target := &loadbalance.Target{
|
||
URL: "http://127.0.0.1:8080",
|
||
Weight: 100,
|
||
}
|
||
target.Healthy.Store(true)
|
||
target.SlowStart = 30 * time.Second
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: "/health",
|
||
SlowStart: 30 * time.Second,
|
||
})
|
||
|
||
// 先标记为健康以初始化慢启动
|
||
checker.MarkHealthy(target)
|
||
|
||
// 标记目标为不健康
|
||
checker.MarkUnhealthy(target)
|
||
|
||
if target.Healthy.Load() {
|
||
t.Error("target 应标记为 unhealthy")
|
||
}
|
||
}
|
||
|
||
// TestMarkHealthy_WithSlowStartManager 测试 MarkHealthy 与 SlowStartManager 集成。
|
||
func TestMarkHealthy_WithSlowStartManager(t *testing.T) {
|
||
target := &loadbalance.Target{
|
||
URL: "http://127.0.0.1:8080",
|
||
Weight: 100,
|
||
}
|
||
target.Healthy.Store(false)
|
||
target.SlowStart = 30 * time.Second
|
||
|
||
checker := NewHealthChecker([]*loadbalance.Target{target}, &config.HealthCheckConfig{
|
||
Interval: 1 * time.Hour,
|
||
Timeout: 5 * time.Second,
|
||
Path: "/health",
|
||
SlowStart: 30 * time.Second,
|
||
})
|
||
|
||
// 标记目标为健康
|
||
checker.MarkHealthy(target)
|
||
|
||
if !target.Healthy.Load() {
|
||
t.Error("target 应标记为 healthy")
|
||
}
|
||
|
||
// 验证慢启动已开始(EffectiveWeight 应被设置为 1)
|
||
ew := target.EffectiveWeight.Load()
|
||
if ew <= 0 {
|
||
t.Errorf("慢启动 EffectiveWeight 应大于 0,got: %d", ew)
|
||
}
|
||
}
|