diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f8450b..8e673b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - native 请求补齐 `Request.TLS` 连接状态;Linux/arm64 容器通过全量 race、vet、build,以及 TLS 并发、Keep-Alive、慢握手和连接上限回收验收。 - 补充慢请求体超时、大请求体和同连接管线请求验收,确认流式 body 读取超时会取消请求并关闭连接,连续请求不会串线。 - 补充 `MaxConnections` 满载拒绝与槽位释放验收,确认请求完成后连接容量可再次使用。 +- 补充 `MaxConcurrentRequests` 过载恢复验收,确认 503 拒绝后请求槽位释放并可继续处理新请求。 - 增加高频建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。 ## v0.6.0-preview diff --git a/ROADMAP.md b/ROADMAP.md index f684b61..38788b8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -47,6 +47,7 @@ - native 长请求已补客户端断开及流式写失败取消,监测退出后再复用 Reader;已验收普通/流式断开、慢 Handler 后连续 Keep-Alive 和管线数据不被误消费。短请求延迟启动监测,已有管线数据时取消依赖后续写失败或 Handler 返回。 - 已补充慢请求头、慢 Content-Length body、慢 chunked body、慢 TLS 握手、大请求体、HTTP/1.0 Keep-Alive、HEAD/204 和管线请求验收;Linux/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json)。 - 已验证 `MaxConnections` 满载时快速拒绝新连接、请求完成后释放容量;后续仍需在目标部署环境观察文件描述符、内核 socket 缓冲和反向代理联动。 +- 已验证 `MaxConcurrentRequests` 满载返回 503,活动请求结束后槽位释放,后续请求可以恢复处理。 - 已用 16 个并发 worker 完成 3,200 次建连/断连 race 验收;MaxConnections 满载拒绝、槽位释放和 TLS 连接上限均已覆盖。 - 新增 `scripts/verifyLinux.py`,固定在 Go 1.25 Linux 容器内运行 race、vet、build 和可选 TLS/非 TLS perfcheck;perfcheck 记录堆、goroutine、连接和 Linux 文件描述符。 - Linux/arm64 已完成 30 秒、128 并发、512 空闲连接的 TLS 与非 TLS 持续回归,报告见 `performance/linuxSoak.json` 和 `performance/linuxSoakTLS.json`,均为 0 错误且连接、goroutine、FD 回收正常。 diff --git a/transport/native/server_test.go b/transport/native/server_test.go index fc41590..b5518e4 100644 --- a/transport/native/server_test.go +++ b/transport/native/server_test.go @@ -722,6 +722,83 @@ func TestServerConcurrentRequestLimit(t *testing.T) { } } +func TestServerConcurrentRequestLimitRecovers(t *testing.T) { + started := make(chan struct{}) + release := make(chan struct{}) + var first sync.Once + application := omni.NewWithRouter(netrouter.New()) + application.GET("/work", func(ctx *omni.Context) error { + first.Do(func() { + close(started) + <-release + }) + return ctx.OK("done") + }) + server := native.New(application) + server.MaxConcurrentRequests = 1 + 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) + + firstConn, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer firstConn.Close() + _, _ = io.WriteString(firstConn, "GET /work HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("first handler did not start") + } + + secondConn, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + _, _ = io.WriteString(secondConn, "GET /work HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") + response, err := http.ReadResponse(bufio.NewReader(secondConn), &http.Request{Method: http.MethodGet}) + if err != nil { + secondConn.Close() + t.Fatal(err) + } + response.Body.Close() + secondConn.Close() + if response.StatusCode != http.StatusServiceUnavailable { + t.Fatalf("expected 503 at concurrency limit, got %d", response.StatusCode) + } + + close(release) + response, err = http.ReadResponse(bufio.NewReader(firstConn), &http.Request{Method: http.MethodGet}) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("expected held request to complete, got %d", response.StatusCode) + } + + thirdConn, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer thirdConn.Close() + _, _ = io.WriteString(thirdConn, "GET /work HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") + response, err = http.ReadResponse(bufio.NewReader(thirdConn), &http.Request{Method: http.MethodGet}) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("request slot was not reusable after overload, got %d", response.StatusCode) + } +} + func TestServerConnectionLimitReleasesAfterRequest(t *testing.T) { started := make(chan struct{}) release := make(chan struct{})