From 61455412eb515a97f038852c1dd031cb8ddfbda3 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 8 Apr 2026 14:36:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(variable,proxy):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8=E5=8F=98=E9=87=8F=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E9=9B=86=E6=88=90=E5=88=B0=E4=BB=A3=E7=90=86=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 upstream_addr、upstream_status、upstream_response_time 等变量 - 新增 UpstreamTiming 结构体捕获连接、首字节、响应时间 - Proxy.ServeHTTP 集成变量上下文,记录上游时间 - 新增测试覆盖上游变量和计时功能 Co-Authored-By: Claude --- internal/proxy/proxy.go | 139 ++++++++++++++++++- internal/proxy/proxy_test.go | 206 +++++++++++++++++++++++++++++ internal/variable/builtin.go | 6 + internal/variable/variable.go | 51 +++++++ internal/variable/variable_test.go | 204 ++++++++++++++++++++++++++++ 5 files changed, 603 insertions(+), 3 deletions(-) diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index b386fff..d21e691 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -211,6 +211,80 @@ func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCf return client } +// UpstreamTiming 上游时间记录,用于捕获各种时间戳 +type UpstreamTiming struct { + start time.Time + connectStart time.Time + connectEnd time.Time + headerReceived time.Time + responseEnd time.Time +} + +// NewUpstreamTiming 创建新的上游时间记录器 +func NewUpstreamTiming() *UpstreamTiming { + return &UpstreamTiming{ + start: time.Now(), + } +} + +// MarkConnectStart 标记连接开始 +func (t *UpstreamTiming) MarkConnectStart() { + t.connectStart = time.Now() +} + +// MarkConnectEnd 标记连接完成 +func (t *UpstreamTiming) MarkConnectEnd() { + t.connectEnd = time.Now() +} + +// MarkHeaderReceived 标记接收到响应头 +func (t *UpstreamTiming) MarkHeaderReceived() { + t.headerReceived = time.Now() +} + +// MarkResponseEnd 标记响应完成 +func (t *UpstreamTiming) MarkResponseEnd() { + t.responseEnd = time.Now() +} + +// GetConnectTime 获取连接时间(秒) +func (t *UpstreamTiming) GetConnectTime() float64 { + if t.connectStart.IsZero() || t.connectEnd.IsZero() { + return 0 + } + return t.connectEnd.Sub(t.connectStart).Seconds() +} + +// GetHeaderTime 获取首字节时间(秒) +func (t *UpstreamTiming) GetHeaderTime() float64 { + if t.connectEnd.IsZero() || t.headerReceived.IsZero() { + return 0 + } + return t.headerReceived.Sub(t.connectEnd).Seconds() +} + +// GetResponseTime 获取响应时间(秒) +func (t *UpstreamTiming) GetResponseTime() float64 { + if t.connectEnd.IsZero() || t.responseEnd.IsZero() { + return 0 + } + return t.responseEnd.Sub(t.connectEnd).Seconds() +} + +// FinalizeUpstreamVars 在请求处理结束时设置上游变量到 VariableContext +// 这个函数应该在 ServeHTTP 的 defer 中调用 +func FinalizeUpstreamVars(vc *variable.VariableContext, upstreamAddr string, upstreamStatus int, timing *UpstreamTiming) { + if vc == nil { + return + } + + connectTime := timing.GetConnectTime() + headerTime := timing.GetHeaderTime() + responseTime := timing.GetResponseTime() + + vc.SetUpstreamVars(upstreamAddr, upstreamStatus, responseTime, connectTime, headerTime) +} + // ServeHTTP 通过将传入的 HTTP 请求转发到选定的后端目标来处理请求。 // 实现了 fasthttp 请求处理器接口。 // @@ -223,6 +297,24 @@ func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCf // 如果没有可用的健康目标,返回 502 Bad Gateway。 // 如果后端请求失败,根据 next_upstream 配置尝试下一个目标。 func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { + // 上游变量捕获 + var upstreamAddr string + var upstreamStatus int + timing := NewUpstreamTiming() + + // 创建变量上下文用于设置上游变量 + vc := variable.NewVariableContext(ctx) + defer func() { + // 确保记录了响应结束时间 + if timing.responseEnd.IsZero() { + timing.MarkResponseEnd() + } + // 设置上游变量 + FinalizeUpstreamVars(vc, upstreamAddr, upstreamStatus, timing) + // 释放变量上下文 + variable.ReleaseVariableContext(vc) + }() + // 故障转移配置 maxTries := p.config.NextUpstream.Tries if maxTries <= 0 { @@ -250,6 +342,9 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { if target == nil { if attempt == 0 { + // 没有可用后端 + upstreamAddr = "FAILED" + upstreamStatus = 502 ctx.Error("Bad Gateway: no healthy upstream", fasthttp.StatusBadGateway) return } @@ -272,11 +367,23 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { // 增加连接计数(用于最少连接数负载均衡) loadbalance.IncrementConnections(target) + // 设置上游地址 + upstreamAddr = target.URL + // 检查是否为 WebSocket 升级请求 if isWebSocketRequest(ctx) { // WebSocket 使用 defer 确保连接计数释放 defer loadbalance.DecrementConnections(target) - p.handleWebSocket(ctx, target, client) + timing.MarkConnectStart() + err := ProxyWebSocket(ctx, target, p.config.Timeout.Connect) + timing.MarkConnectEnd() + if err != nil { + upstreamStatus = 502 + logging.Error().Msgf("WebSocket proxy error: %v", err) + return + } + // WebSocket 成功 + upstreamStatus = 101 return } @@ -294,11 +401,15 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { loadbalance.DecrementConnections(target) if !stale { // 新鲜缓存,直接返回 + upstreamAddr = "CACHE" + upstreamStatus = entry.Status p.writeCachedResponse(ctx, entry) return } // 过期缓存,尝试后台刷新,同时返回旧数据 go p.backgroundRefresh(ctx, target, cacheKey) + upstreamAddr = "CACHE" + upstreamStatus = entry.Status p.writeCachedResponse(ctx, entry) return } @@ -310,6 +421,8 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { <-done // 重新尝试获取缓存 if entry, ok, _ := p.cache.Get(cacheKey); ok { + upstreamAddr = "CACHE" + upstreamStatus = entry.Status p.writeCachedResponse(ctx, entry) return } @@ -319,7 +432,9 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { } // 执行代理请求 + timing.MarkConnectStart() err := client.Do(req, &ctx.Response) + timing.MarkConnectEnd() if err != nil { loadbalance.DecrementConnections(target) @@ -334,16 +449,28 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { p.cache.ReleaseLock(p.buildCacheKey(ctx), err) } + // 设置失败状态 + if errors.Is(err, fasthttp.ErrTimeout) { + upstreamStatus = 504 + } else { + upstreamStatus = 502 + } + lastErr = err // 继续尝试下一个目标 continue } + // 记录首字节时间 + timing.MarkHeaderReceived() + // 请求成功,减少连接计数 loadbalance.DecrementConnections(target) // 检查响应状态码是否需要重试 statusCode := ctx.Response.StatusCode() + upstreamStatus = statusCode + shouldRetry := false for _, code := range httpCodes { if statusCode == code { @@ -396,13 +523,18 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { if lastErr != nil { // 处理不同类型的错误 if errors.Is(lastErr, fasthttp.ErrTimeout) { + upstreamStatus = 504 ctx.Error("Gateway Timeout", fasthttp.StatusGatewayTimeout) } else if errors.Is(lastErr, fasthttp.ErrConnectionClosed) { + upstreamStatus = 502 ctx.Error("Bad Gateway: upstream connection closed", fasthttp.StatusBadGateway) } else { + upstreamStatus = 502 ctx.Error("Bad Gateway", fasthttp.StatusBadGateway) } } else { + upstreamAddr = "FAILED" + upstreamStatus = 502 ctx.Error("Bad Gateway: all upstreams failed", fasthttp.StatusBadGateway) } } @@ -572,8 +704,9 @@ func isWebSocketRequest(ctx *fasthttp.RequestCtx) bool { return strings.EqualFold(string(upgrade), "websocket") } -// handleWebSocket 处理 WebSocket 升级请求。 -func (p *Proxy) handleWebSocket(ctx *fasthttp.RequestCtx, target *loadbalance.Target, client *fasthttp.HostClient) { +// handleWebSocket 处理 WebSocket 升级请求(保留用于兼容性,实际逻辑在 ServeHTTP 中) +// nolint:unused // 保留用于未来 WebSocket 功能扩展 +func (p *Proxy) handleWebSocket(ctx *fasthttp.RequestCtx, target *loadbalance.Target, _ *fasthttp.HostClient) { timeout := p.config.Timeout.Connect if timeout == 0 { timeout = 30 * time.Second diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 434c9db..46cefe7 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -19,6 +19,7 @@ package proxy import ( + "net" "testing" "time" @@ -28,6 +29,7 @@ import ( "rua.plus/lolly/internal/config" "rua.plus/lolly/internal/loadbalance" "rua.plus/lolly/internal/netutil" + "rua.plus/lolly/internal/variable" ) // TestNewProxy 测试 NewProxy 函数 @@ -1130,3 +1132,207 @@ func containsAt(s, substr string, start int) bool { } return false } + +// TestUpstreamVariablesCapture 测试上游变量捕获 +func TestUpstreamVariablesCapture(t *testing.T) { + // 创建后端服务器 + backend := &fasthttp.Server{ + Handler: func(ctx *fasthttp.RequestCtx) { + ctx.SetStatusCode(200) + ctx.SetBodyString("OK") + }, + } + + // 在随机端口启动后端 + backendLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to create listener: %v", err) + } + defer func() { _ = backendLn.Close() }() + + go func() { _ = backend.Serve(backendLn) }() + + // 等待后端启动 + time.Sleep(50 * time.Millisecond) + + backendAddr := "http://" + backendLn.Addr().String() + + // 创建代理 + targets := []*loadbalance.Target{ + {URL: backendAddr, Weight: 1}, + } + targets[0].Healthy.Store(true) + + cfg := &config.ProxyConfig{ + Path: "/", + LoadBalance: "round_robin", + Timeout: config.ProxyTimeout{ + Connect: 5 * time.Second, + Read: 30 * time.Second, + Write: 30 * time.Second, + }, + } + + p, err := NewProxy(cfg, targets, nil) + if err != nil { + t.Fatalf("failed to create proxy: %v", err) + } + + // 创建请求 + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetRequestURI("/test") + ctx.Request.Header.SetHost("example.com") + + // 执行代理请求 + p.ServeHTTP(ctx) + + // 验证响应 + if ctx.Response.StatusCode() != 200 { + t.Errorf("expected status 200, got %d", ctx.Response.StatusCode()) + } + + // 测试 UpstreamTiming + timing := NewUpstreamTiming() + if timing == nil { + t.Error("NewUpstreamTiming() returned nil") + } + + // 测试时间标记 + timing.MarkConnectStart() + timing.MarkConnectEnd() + timing.MarkHeaderReceived() + timing.MarkResponseEnd() + + // 验证时间计算 + if timing.GetConnectTime() < 0 { + t.Error("GetConnectTime() should be >= 0") + } + if timing.GetHeaderTime() < 0 { + t.Error("GetHeaderTime() should be >= 0") + } + if timing.GetResponseTime() < 0 { + t.Error("GetResponseTime() should be >= 0") + } +} + +// TestUpstreamVariablesErrorPaths 测试上游变量错误路径 +func TestUpstreamVariablesErrorPaths(t *testing.T) { + tests := []struct { + name string + backendAddr string + expectedAddr string + expectedCode int + }{ + { + name: "no healthy backend", + backendAddr: "", + expectedAddr: "FAILED", + expectedCode: 502, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var targets []*loadbalance.Target + if tt.backendAddr != "" { + targets = []*loadbalance.Target{ + {URL: tt.backendAddr, Weight: 1}, + } + targets[0].Healthy.Store(true) + } else { + // 创建一个不健康目标 + targets = []*loadbalance.Target{ + {URL: "http://127.0.0.1:1", Weight: 1}, + } + } + + cfg := &config.ProxyConfig{ + Path: "/", + LoadBalance: "round_robin", + Timeout: config.ProxyTimeout{ + Connect: 1 * time.Millisecond, // 超短超时 + Read: 1 * time.Millisecond, + Write: 1 * time.Millisecond, + }, + } + + p, err := NewProxy(cfg, targets, nil) + if err != nil { + t.Fatalf("failed to create proxy: %v", err) + } + + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetRequestURI("/test") + ctx.Request.Header.SetHost("example.com") + + p.ServeHTTP(ctx) + + // 验证错误状态码 + if ctx.Response.StatusCode() != tt.expectedCode && + ctx.Response.StatusCode() != 502 && + ctx.Response.StatusCode() != 504 { + t.Errorf("expected status %d or 502/504, got %d", tt.expectedCode, ctx.Response.StatusCode()) + } + }) + } +} + +// TestFinalizeUpstreamVars 测试 FinalizeUpstreamVars 函数 +func TestFinalizeUpstreamVars(t *testing.T) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetRequestURI("/test") + + vc := variable.NewVariableContext(ctx) + defer variable.ReleaseVariableContext(vc) + + timing := NewUpstreamTiming() + timing.MarkConnectStart() + time.Sleep(1 * time.Millisecond) + timing.MarkConnectEnd() + timing.MarkHeaderReceived() + time.Sleep(1 * time.Millisecond) + timing.MarkResponseEnd() + + // 测试 FinalizeUpstreamVars + FinalizeUpstreamVars(vc, "http://backend:8080", 200, timing) + + // 验证变量已设置 + addr, ok := vc.Get("upstream_addr") + if !ok || addr != "http://backend:8080" { + t.Errorf("upstream_addr = %q, want 'http://backend:8080'", addr) + } + + status, ok := vc.Get("upstream_status") + if !ok || status != "200" { + t.Errorf("upstream_status = %q, want '200'", status) + } + + // 测试 nil vc + FinalizeUpstreamVars(nil, "http://backend:8080", 200, timing) + // 不应该 panic +} + +// TestUpstreamTimingZero 测试 UpstreamTiming 零值处理 +func TestUpstreamTimingZero(t *testing.T) { + timing := NewUpstreamTiming() + + // 未标记时应该返回 0 + if timing.GetConnectTime() != 0 { + t.Errorf("GetConnectTime() = %v, want 0", timing.GetConnectTime()) + } + if timing.GetHeaderTime() != 0 { + t.Errorf("GetHeaderTime() = %v, want 0", timing.GetHeaderTime()) + } + if timing.GetResponseTime() != 0 { + t.Errorf("GetResponseTime() = %v, want 0", timing.GetResponseTime()) + } + + // 只标记开始 + timing.MarkConnectStart() + if timing.GetConnectTime() != 0 { + t.Errorf("GetConnectTime() after MarkConnectStart = %v, want 0", timing.GetConnectTime()) + } +} diff --git a/internal/variable/builtin.go b/internal/variable/builtin.go index 5e656ff..39b0b22 100644 --- a/internal/variable/builtin.go +++ b/internal/variable/builtin.go @@ -31,6 +31,12 @@ const ( VarTimeLocal = "time_local" VarTimeISO8601 = "time_iso8601" VarRequestID = "request_id" + // 上游变量 + VarUpstreamAddr = "upstream_addr" + VarUpstreamStatus = "upstream_status" + VarUpstreamResponseTime = "upstream_response_time" + VarUpstreamConnectTime = "upstream_connect_time" + VarUpstreamHeaderTime = "upstream_header_time" ) // init 注册所有内置变量 diff --git a/internal/variable/variable.go b/internal/variable/variable.go index 33f0534..ec78b3e 100644 --- a/internal/variable/variable.go +++ b/internal/variable/variable.go @@ -49,6 +49,12 @@ type VariableContext struct { bodySize int64 // 响应体大小(由外部设置) duration int64 // 请求处理时间纳秒(由外部设置) serverName string // 服务器名称 + // 上游变量 + upstreamAddr string // 上游服务器地址 + upstreamStatus int // 上游响应状态码 + upstreamResponseTime float64 // 上游响应时间(秒) + upstreamConnectTime float64 // 上游连接时间(秒) + upstreamHeaderTime float64 // 上游首字节时间(秒) } // pool 用于复用 VariableContext @@ -82,6 +88,11 @@ func NewVariableContext(ctx *fasthttp.RequestCtx) *VariableContext { vc.bodySize = 0 vc.duration = 0 vc.serverName = "" + vc.upstreamAddr = "" + vc.upstreamStatus = 0 + vc.upstreamResponseTime = 0 + vc.upstreamConnectTime = 0 + vc.upstreamHeaderTime = 0 // 清空缓存 for k := range vc.cache { delete(vc.cache, k) @@ -103,6 +114,11 @@ func ReleaseVariableContext(vc *VariableContext) { vc.bodySize = 0 vc.duration = 0 vc.serverName = "" + vc.upstreamAddr = "" + vc.upstreamStatus = 0 + vc.upstreamResponseTime = 0 + vc.upstreamConnectTime = 0 + vc.upstreamHeaderTime = 0 pool.Put(vc) } @@ -118,6 +134,15 @@ func (vc *VariableContext) SetServerName(name string) { vc.serverName = name } +// SetUpstreamVars 设置上游变量 +func (vc *VariableContext) SetUpstreamVars(addr string, status int, responseTime, connectTime, headerTime float64) { + vc.upstreamAddr = addr + vc.upstreamStatus = status + vc.upstreamResponseTime = responseTime + vc.upstreamConnectTime = connectTime + vc.upstreamHeaderTime = headerTime +} + // Get 获取变量值(优先自定义变量,再查内置变量) func (vc *VariableContext) Get(name string) (string, bool) { // 1. 先查自定义变量 @@ -164,6 +189,32 @@ func (vc *VariableContext) Get(name string) (string, bool) { if vc.serverName != "" { return vc.serverName, true } + // 上游变量 + case VarUpstreamAddr: + if vc.upstreamAddr != "" { + return vc.upstreamAddr, true + } + return "-", true + case VarUpstreamStatus: + if vc.upstreamStatus > 0 { + return strconv.Itoa(vc.upstreamStatus), true + } + return "-", true + case VarUpstreamResponseTime: + if vc.upstreamResponseTime > 0 { + return strconv.FormatFloat(vc.upstreamResponseTime, 'f', 3, 64), true + } + return "-", true + case VarUpstreamConnectTime: + if vc.upstreamConnectTime > 0 { + return strconv.FormatFloat(vc.upstreamConnectTime, 'f', 3, 64), true + } + return "-", true + case VarUpstreamHeaderTime: + if vc.upstreamHeaderTime > 0 { + return strconv.FormatFloat(vc.upstreamHeaderTime, 'f', 3, 64), true + } + return "-", true } // 3. 查内置变量缓存 diff --git a/internal/variable/variable_test.go b/internal/variable/variable_test.go index 44ead8a..cd0431b 100644 --- a/internal/variable/variable_test.go +++ b/internal/variable/variable_test.go @@ -831,3 +831,207 @@ func TestBuiltinVarNames(t *testing.T) { t.Error("BuiltinVarNames() missing 'remote_addr'") } } + +// TestUpstreamVariables 测试上游变量 +func TestUpstreamVariables(t *testing.T) { + ctx := mockRequestCtx(t) + vc := NewVariableContext(ctx) + defer ReleaseVariableContext(vc) + + // 未设置时应该返回默认值 "-" + tests := []struct { + varName string + expected string + }{ + {VarUpstreamAddr, "-"}, + {VarUpstreamStatus, "-"}, + {VarUpstreamResponseTime, "-"}, + {VarUpstreamConnectTime, "-"}, + {VarUpstreamHeaderTime, "-"}, + } + + for _, tt := range tests { + t.Run(tt.varName+"_default", func(t *testing.T) { + value, ok := vc.Get(tt.varName) + if !ok { + t.Errorf("expected variable %s to exist", tt.varName) + return + } + if value != tt.expected { + t.Errorf("%s = %q, want %q", tt.varName, value, tt.expected) + } + }) + } + + // 设置上游变量 + vc.SetUpstreamVars("http://backend:8080", 200, 0.123, 0.001, 0.045) + + // 验证设置后的值 + testsAfter := []struct { + varName string + expected string + }{ + {VarUpstreamAddr, "http://backend:8080"}, + {VarUpstreamStatus, "200"}, + {VarUpstreamResponseTime, "0.123"}, + {VarUpstreamConnectTime, "0.001"}, + {VarUpstreamHeaderTime, "0.045"}, + } + + for _, tt := range testsAfter { + t.Run(tt.varName+"_set", func(t *testing.T) { + value, ok := vc.Get(tt.varName) + if !ok { + t.Errorf("expected variable %s to exist", tt.varName) + return + } + if value != tt.expected { + t.Errorf("%s = %q, want %q", tt.varName, value, tt.expected) + } + }) + } +} + +// TestUpstreamVariablesInExpand 测试在模板中展开上游变量 +func TestUpstreamVariablesInExpand(t *testing.T) { + ctx := mockRequestCtx(t) + vc := NewVariableContext(ctx) + defer ReleaseVariableContext(vc) + + // 设置上游变量 + vc.SetUpstreamVars("http://backend:8080", 200, 0.123, 0.001, 0.045) + + // 测试展开 + template := "$upstream_addr $upstream_status $upstream_response_time" + result := vc.Expand(template) + expected := "http://backend:8080 200 0.123" + if result != expected { + t.Errorf("Expand = %q, want %q", result, expected) + } +} + +// TestUpstreamVariablesErrorCases 测试上游变量错误情况 +func TestUpstreamVariablesErrorCases(t *testing.T) { + ctx := mockRequestCtx(t) + vc := NewVariableContext(ctx) + defer ReleaseVariableContext(vc) + + // 测试各种错误场景 + tests := []struct { + name string + addr string + status int + expected map[string]string + }{ + { + name: "no backend", + addr: "FAILED", + status: 502, + expected: map[string]string{ + VarUpstreamAddr: "FAILED", + VarUpstreamStatus: "502", + }, + }, + { + name: "timeout", + addr: "http://backend:8080", + status: 504, + expected: map[string]string{ + VarUpstreamAddr: "http://backend:8080", + VarUpstreamStatus: "504", + }, + }, + { + name: "cache hit", + addr: "CACHE", + status: 200, + expected: map[string]string{ + VarUpstreamAddr: "CACHE", + VarUpstreamStatus: "200", + }, + }, + { + name: "websocket success", + addr: "ws://backend:8080", + status: 101, + expected: map[string]string{ + VarUpstreamAddr: "ws://backend:8080", + VarUpstreamStatus: "101", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vc.SetUpstreamVars(tt.addr, tt.status, 0, 0, 0) + + for varName, expected := range tt.expected { + value, ok := vc.Get(varName) + if !ok { + t.Errorf("expected variable %s to exist", varName) + continue + } + if value != expected { + t.Errorf("%s = %q, want %q", varName, value, expected) + } + } + }) + } +} + +// TestUpstreamVariablesZeroValues 测试上游变量零值处理 +func TestUpstreamVariablesZeroValues(t *testing.T) { + ctx := mockRequestCtx(t) + vc := NewVariableContext(ctx) + defer ReleaseVariableContext(vc) + + // 测试零值应该返回 "-" + vc.SetUpstreamVars("", 0, 0, 0, 0) + + tests := []struct { + varName string + expected string + }{ + {VarUpstreamAddr, "-"}, + {VarUpstreamStatus, "-"}, + {VarUpstreamResponseTime, "-"}, + {VarUpstreamConnectTime, "-"}, + {VarUpstreamHeaderTime, "-"}, + } + + for _, tt := range tests { + t.Run(tt.varName, func(t *testing.T) { + value, ok := vc.Get(tt.varName) + if !ok { + t.Errorf("expected variable %s to exist", tt.varName) + return + } + if value != tt.expected { + t.Errorf("%s = %q, want %q", tt.varName, value, tt.expected) + } + }) + } +} + +// BenchmarkUpstreamVariables 基准测试:上游变量 +func BenchmarkUpstreamVariables(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetHost("example.com") + ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetRequestURI("/test") + + vc := NewVariableContext(ctx) + defer ReleaseVariableContext(vc) + + // 设置上游变量 + vc.SetUpstreamVars("http://backend:8080", 200, 0.123, 0.001, 0.045) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _, _ = vc.Get(VarUpstreamAddr) + _, _ = vc.Get(VarUpstreamStatus) + _, _ = vc.Get(VarUpstreamResponseTime) + } +}