test: cover native slow bodies and pipelining

This commit is contained in:
2026-09-21 03:03:27 +08:00
parent 22939fdb0b
commit 241a568c83
3 changed files with 82 additions and 0 deletions

View File

@@ -5,6 +5,7 @@
- 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。 - 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。
- 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。 - 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。
- 加固 native HTTP/1.1 解析边界:校验 Host、重复 Content-Length、Connection token、Transfer-Encoding、控制字符和响应头名称;补充 HTTP/1.0 Keep-Alive、HEAD、204 以及异常请求测试。 - 加固 native HTTP/1.1 解析边界:校验 Host、重复 Content-Length、Connection token、Transfer-Encoding、控制字符和响应头名称;补充 HTTP/1.0 Keep-Alive、HEAD、204 以及异常请求测试。
- 补充慢请求体超时、大请求体和同连接管线请求验收,确认请求体未完整读取时不会进入业务 Handler,连续请求不会串线。
## v0.5.0 ## v0.5.0

View File

@@ -42,6 +42,7 @@
- 标准库与 native 共用 TCP benchmark,固定 1 / 32 / 128 并发;相同应用、客户端、超时和响应检查,避免两个测试入口逐渐失去可比性。 - 标准库与 native 共用 TCP benchmark,固定 1 / 32 / 128 并发;相同应用、客户端、超时和响应检查,避免两个测试入口逐渐失去可比性。
- 新增独立服务进程压测 `internal/perfcheck`,记录成功吞吐、P50/P95/P99、服务端分配、采样堆峰值,以及 128 / 1,024 条空闲长连接的堆内存、goroutine 和断开后的回收情况。原始报告保存在 `performance/tcpBaseline.json`。 - 新增独立服务进程压测 `internal/perfcheck`,记录成功吞吐、P50/P95/P99、服务端分配、采样堆峰值,以及 128 / 1,024 条空闲长连接的堆内存、goroutine 和断开后的回收情况。原始报告保存在 `performance/tcpBaseline.json`。
- native 当前拒绝 chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。 - native 当前拒绝 chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。
- 已补充慢请求体超时、大请求体、HTTP/1.0 Keep-Alive、HEAD/204 和管线请求验收;Linux、慢客户端规模、TLS 和持续运行压测仍待在目标环境验证。
- 已具备全量 race、vet、build 验收;生产压测、长连接规模和真实部署验证仍待完成。 - 已具备全量 race、vet、build 验收;生产压测、长连接规模和真实部署验证仍待完成。
- 自动验收入口:`python3 scripts/verifyPerformance.py`;增加 `--load --output /tmp/goOmniPerf.json` 可重复运行独立进程并发与内存矩阵。 - 自动验收入口:`python3 scripts/verifyPerformance.py`;增加 `--load --output /tmp/goOmniPerf.json` 可重复运行独立进程并发与内存矩阵。

View File

@@ -9,6 +9,7 @@ import (
"net" "net"
"net/http" "net/http"
"strings" "strings"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -88,6 +89,85 @@ func TestServerRejectsUnsupportedBodyAndLimits(t *testing.T) {
} }
} }
func TestServerSlowBodyTimesOutBeforeHandler(t *testing.T) {
var handlerCalls atomic.Int32
application := omni.NewWithRouter(netrouter.New())
application.POST("/body", func(ctx *omni.Context) error {
handlerCalls.Add(1)
return ctx.OK("ok")
})
server := native.New(application)
server.ReadTimeout = 30 * time.Millisecond
server.IdleTimeout = time.Second
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- server.Serve(context.Background(), listener) }()
waitForServer(t, listener.Addr().String())
defer stopServer(t, server, listener, done)
conn, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(time.Second))
_, _ = io.WriteString(conn, "POST /body HTTP/1.1\r\nHost: test\r\nContent-Length: 4\r\n\r\na")
buffer := make([]byte, 1)
if _, err := conn.Read(buffer); err == nil {
t.Fatal("expected slow request body to time out and close")
}
if handlerCalls.Load() != 0 {
t.Fatal("handler ran before the request body was complete")
}
}
func TestServerPipelinedRequestsAndLargeBody(t *testing.T) {
application := omni.NewWithRouter(netrouter.New())
application.POST("/body", func(ctx *omni.Context) error {
body, err := io.ReadAll(ctx.Request().Body)
if err != nil {
return err
}
return ctx.OK(map[string]int{"length": len(body)})
})
application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") })
server, listener, done := startServer(t, application)
defer stopServer(t, server, listener, done)
body := strings.Repeat("x", 64<<10)
conn, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
reader := bufio.NewReader(conn)
request := fmt.Sprintf("POST /body HTTP/1.1\r\nHost: test\r\nConnection: keep-alive\r\nContent-Length: %d\r\n\r\n%sGET /health HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n", len(body), body)
if _, err := io.WriteString(conn, request); err != nil {
t.Fatal(err)
}
first, err := http.ReadResponse(reader, &http.Request{Method: http.MethodPost})
if err != nil {
t.Fatal(err)
}
firstBody, err := io.ReadAll(first.Body)
first.Body.Close()
if err != nil || first.StatusCode != http.StatusOK || !strings.Contains(string(firstBody), `"length":65536`) {
t.Fatalf("unexpected large body response: status=%d body=%s error=%v", first.StatusCode, firstBody, err)
}
second, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet})
if err != nil {
t.Fatal(err)
}
secondBody, err := io.ReadAll(second.Body)
second.Body.Close()
if err != nil || second.StatusCode != http.StatusOK || !strings.Contains(string(secondBody), `"ok"`) {
t.Fatalf("unexpected pipelined response: status=%d body=%s error=%v", second.StatusCode, secondBody, err)
}
}
func TestServerProtocolBoundaries(t *testing.T) { func TestServerProtocolBoundaries(t *testing.T) {
application := omni.NewWithRouter(netrouter.New()) application := omni.NewWithRouter(netrouter.New())
application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") })