From c5e415ca529122f3877f821579f88bbd91c7ebbd Mon Sep 17 00:00:00 2001 From: k2safe Date: Tue, 22 Sep 2026 01:38:42 +0800 Subject: [PATCH] release: prepare v0.6.0-preview --- CHANGELOG.md | 10 +- PERFORMANCE.md | 19 +- README.md | 8 +- ROADMAP.md | 11 +- cmd/go-omni/README.md | 8 +- cmd/go-omni/scaffold/version.go | 2 +- internal/perfcheck/main.go | 92 ++++++---- performance/linuxSoak.json | 265 +++++++++++++++++++++++++++ performance/linuxSoakTLS.json | 265 +++++++++++++++++++++++++++ scripts/verifyBusiness.py | 2 +- scripts/verifyDatabase.py | 2 +- scripts/verifyLinux.py | 84 +++++++++ scripts/verifyPerformance.py | 5 +- scripts/verifyScaffold.py | 2 +- scripts/verifyTransports.py | 2 +- transport/README.md | 2 +- transport/native/server_test.go | 8 +- transport/native/slow_client_test.go | 72 ++++++++ 18 files changed, 797 insertions(+), 62 deletions(-) create mode 100644 performance/linuxSoak.json create mode 100644 performance/linuxSoakTLS.json create mode 100755 scripts/verifyLinux.py create mode 100644 transport/native/slow_client_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9424a..ac2a526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,15 @@ - native 请求补齐 `Request.TLS` 连接状态;Linux/arm64 容器通过全量 race、vet、build,以及 TLS 并发、Keep-Alive、慢握手和连接上限回收验收。 - 补充慢请求体超时、大请求体和同连接管线请求验收,确认请求体未完整读取时不会进入业务 Handler,连续请求不会串线。 - 补充 `MaxConnections` 满载拒绝与槽位释放验收,确认请求完成后连接容量可再次使用。 -- 增加 8 并发、800 次建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。 +- 增加高频建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。 + +## v0.6.0-preview + +- native 网络层完成慢请求头、慢 Content-Length / chunked body、慢 TLS 握手和 TLS 请求元数据验收,未完整读取的请求不会进入业务 Handler。 +- 将 native 连接 churn race 验收扩展为 16 个 worker、3,200 次建连/断连,并覆盖明文与 TLS 的连接上限、Keep-Alive 和槽位回收。 +- perfcheck 支持最长 30 分钟持续场景,新增 Linux 文件描述符采样;固定记录服务端堆、goroutine、连接数、accept 数和 FD 回收状态。 +- 新增 `scripts/verifyLinux.py`,在 `golang:1.25` Linux 容器内统一执行 race、vet、build 和可选 TLS / 非 TLS 压测。 +- 完成 Linux/arm64 30 秒高并发回归:128 并发流量、512 条空闲连接,TLS 与非 TLS 均为 0 错误,连接、goroutine 和 FD 回收正常;原始报告见 `performance/linuxSoak.json` 和 `performance/linuxSoakTLS.json`。 ## v0.5.0 diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 2822ab8..84a706b 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -13,13 +13,25 @@ python3 scripts/verifyPerformance.py --load --output /tmp/goOmniPerf.json # 只运行独立进程矩阵,默认每档 2 秒、3 轮 go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.json + +# 在 Linux Docker 容器内固定运行 race、vet、build 和 TLS 压测 +python3 scripts/verifyLinux.py --load --tls --duration 30s \ + --concurrency 128 --idle 512 --count 1 \ + --output performance/linuxSoakTLS.json + +# 发布候选验收可把持续流量延长到 30 分钟 +python3 scripts/verifyLinux.py --load --tls --duration 30m \ + --concurrency 128 --idle 512 --count 1 \ + --output performance/linuxSoakTLS-30m.json ``` 可通过 `--concurrency 1,32,128` / `--idle 128,1024` 调整矩阵;传空字符串可跳过对应场景。工具只监听本机随机端口,每个场景启动独立服务进程,并在结束后关闭客户端、检查连接排空、关闭服务。响应状态、完整响应体、requestId 或传输错误均计入失败,不能把快速返回的错误页算成成功吞吐。 +Linux 验收脚本使用 `golang:1.25` 容器,固定执行 `go test -race ./...`、`go vet ./...`、`go build ./...`,再按参数运行 perfcheck。perfcheck 在 Linux 读取 `/proc/self/fd`,报告连接建立期间的文件描述符峰值以及回收后的数量;同时记录服务端堆、goroutine、连接数和累计 accept 数。30 秒报告用于快速回归,30 分钟命令用于发布前持续压测。 + ## 2026-09-21 本机测量 -环境:Go 1.25.0、darwin/arm64、Apple M2 Max、GOMAXPROCS=12。网络实现来自 `3d5f225` 工作树(native 与 v0.5.0 相同);本次新增测量工具,不修改服务端实现。原始测量值见 [tcpBaseline.json](performance/tcpBaseline.json)。报告时间为 UTC,北京时间是 9 月 21 日。 +环境:Go 1.25.0、darwin/arm64、Apple M2 Max、GOMAXPROCS=12。网络实现来自 `3d5f225` 工作树(native 与 v0.6.0-preview 相同);本次新增测量工具,不修改服务端实现。原始测量值见 [tcpBaseline.json](performance/tcpBaseline.json)。报告时间为 UTC,北京时间是 9 月 21 日。 每个并发档位持续约 2 秒、重复 3 轮,每轮交替标准库/native 的先后顺序。下表为各指标的三轮中位数;全部流量场景共完成 **2,660,828** 次请求,错误 **0**。另有 128 / 1,024 条空闲长连接的建立、保持和回收测试。 @@ -48,6 +60,7 @@ go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.jso - 并发模型为闭环:每个 worker 等上一个请求完成后发下一个。实际经过时间包含最后请求排空;吞吐只计算成功响应。不能据此推断开放到达流量下的排队或过载表现。 - 延迟从客户端发起请求到完整响应校验完成,包含失败请求。直方图使用每个 2 的幂区间内 16 档,报告桶上界,舍入误差至多约 6.25%。小幅差别不应过度解释。 - 服务端 `runtime.MemStats` 通过独立 stdin/stdout 控制通道读取,客户端内存不计入。累计分配差值包含少量采样开销。堆峰值每 200 ms 采样,可能错过瞬时峰值;这些指标不是进程 RSS。 +- Linux 文件描述符来自服务端子进程的 `/proc/self/fd`,包含标准输入输出、监听 socket 和连接;macOS 等没有该路径的平台会省略该字段。回收后的数量应回到启动附近,不能代替内核 socket 缓冲或 RSS 测量。 - 空闲场景在所有连接完成健康请求后做 GC 再采样。新增堆内存包括连接、缓冲区及分摊的运行时开销。`after` 为客户端断开并排空后一次 GC,`afterReclaim` 为第二次 GC,用于观察对象池缓存释放。 - native 配置上限为 8,192 连接 / 4,096 活跃请求;本轮并发低于上限,未验证过载保护容量。标准库没有同样的并发闸门;比较包括各自真实实现的开销。 - 可用 `python3 scripts/verifyPerformance.py --load --tls --duration 2s` 运行 TLS 版本的独立进程矩阵。该模式在每个子进程生成短期自签名证书,客户端仅用于本机基准验证;本报告的历史表格仍是无 TLS 数据。 @@ -60,10 +73,12 @@ macOS arm64 本机的 5 秒 TLS 矩阵见 [tlsBaseline.json](performance/tlsBase Linux arm64 容器使用 Go 1.25.14、GOMAXPROCS=4 运行 3 秒矩阵,见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json):1 / 32 / 128 并发和 128 / 512 条空闲连接均为 0 错误;native 在 128 并发约 99,876 req/s,512 条空闲 TLS 连接的服务端堆增量约 4.47 MiB,连接关闭后 goroutine 回到基线。容器验证同时通过全量 race、vet、build。 +Linux arm64 的 30 秒高并发回归见 [linuxSoak.json](performance/linuxSoak.json) 和 [linuxSoakTLS.json](performance/linuxSoakTLS.json):两份报告均使用 128 并发流量和 512 条空闲连接,所有场景错误为 0;native 非 TLS 约 146,614 req/s、TLS 约 149,720 req/s,空闲连接堆增量分别约 2.57 MiB 和 4.47 MiB,FD 均由 7 增至 519 并回到 7,goroutine 均回到启动附近。 + `BenchmarkTransportTCPApplicationRequest/{nethttp,native}/concurrency{1,32,128}` 使用同一测试入口、客户端、应用与响应检查。原先独立的 `BenchmarkNativeApplicationRequest` 已合并,避免两个入口配置漂移。`verifyPerformance.py` 会一并运行。 并行 benchmark 的 **`ns/op` 是总经过时间除以请求数,反映吞吐成本,不是单请求延迟**。`B/op` / `allocs/op` 统计同一进程中的客户端与服务端总分配。此前报告的 native 15.4–16.7 µs/op、约 7.8 KB/op、100 allocs/op 应按这个含义理解,不能作为服务端延迟或独立内存指标。 `BenchmarkTransportRouteDispatch` 和 `BenchmarkTransportApplicationRequest` 继续保留,分别测进程内路由分发和完整框架处理链;不包含 TCP,不能直接与上述网络延迟比较。 -后续优先补 Linux 目标环境、持续压测、大请求体/慢客户端、连接反复建立回收和协议边界验证,再通过 profile 决定是否优化对象分配或事件状态机。 +后续通过 30 分钟命令完成发布前持续压测,并在目标反向代理和部署环境继续观察内核 socket 缓冲、FD 上限与真实业务流量;再通过 profile 决定是否优化对象分配或事件状态机。 diff --git a/README.md b/README.md index c9f60d9..3237cf7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认使用 远程 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。业务项目可以直接引用指定版本: ```sh -go get git.yuntuo.hk/k2safe/go-omni@v0.5.0 +go get git.yuntuo.hk/k2safe/go-omni@v0.6.0-preview ``` `v0.4.0` 移除了 Gin 和 Gin 专属的 `WithMode` 配置;`omni.New()` 直接使用标准库路由,已有业务 Handler / Service 无需修改。 @@ -13,7 +13,7 @@ go get git.yuntuo.hk/k2safe/go-omni@v0.5.0 创建新业务项目: ```sh -go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.5.0 +go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.6.0-preview go-omni new --module example.com/billing ./billing cd billing go mod tidy @@ -200,10 +200,10 @@ userService := service.NewUsers(services) 从远程版本生成并升级一个独立业务模块,可运行: ```sh -python3 scripts/verifyBusiness.py --version v0.5.0 +python3 scripts/verifyBusiness.py --version v0.6.0-preview ``` -该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.5.0`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 +该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.6.0-preview`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 标准库与 native 的并发、延迟和长连接内存对照见 [PERFORMANCE.md](PERFORMANCE.md)。`python3 scripts/verifyPerformance.py` 运行检查与 benchmark;增加 `--load --output /tmp/goOmniPerf.json` 可测量独立服务进程并保存报告。 diff --git a/ROADMAP.md b/ROADMAP.md index 746cf16..b764c77 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -35,7 +35,7 @@ - 业务试用没有要求修改 Handler / Service;现有公共 API 足以支撑一套分层业务模块。 - 自动验收入口:`python3 scripts/verifyBusiness.py --version v0.4.0`。 -## 5. 自研网络层、稳定性与性能基线(进行中,v0.5.0 预览) +## 5. 自研网络层、稳定性与性能基线(v0.6.0-preview 验收完成) - 已加入标准库路由和完整 GoOmni 请求链 benchmark,输出分配次数和耗时,作为未来自研网络层的对照基线。 - 新增 `transport/native` 第一版自研 HTTP/1.1 服务端:自行管理 TCP listener、Keep-Alive、Content-Length / chunked 请求解析、响应写回、读写超时和优雅关闭;通过 `App.RunWith` 接入,业务 Handler / Service 保持不变。 @@ -44,10 +44,11 @@ - native 已支持受 `MaxBodyBytes` 约束的 `Content-Length` / chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。 - native 响应已支持标准 `http.Flusher`;HTTP/1.1 流式响应使用 chunked framing,普通响应继续走 `Content-Length`,HTTP/1.0 回退到关闭连接分帧。 - native 长请求已补客户端断开及流式写失败取消,监测退出后再复用 Reader;已验收普通/流式断开、慢 Handler 后连续 Keep-Alive 和管线数据不被误消费。短请求延迟启动监测,已有管线数据时取消依赖后续写失败或 Handler 返回。 -- 已补充慢请求体超时、大请求体、HTTP/1.0 Keep-Alive、HEAD/204、管线请求和基础 TLS 客户端验收;Linux/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json);慢客户端规模和持续生产压测仍待真实部署环境验证。 +- 已补充慢请求头、慢 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 缓冲和反向代理联动。 -- 已用 8 个并发 worker 完成 800 次建连/断连 race 验收;真实部署仍需补文件描述符和跨平台 socket 行为观测。 -- 已具备全量 race、vet、build 验收;生产压测、长连接规模和真实部署验证仍待完成。 -- 自动验收入口:`python3 scripts/verifyPerformance.py`;增加 `--load --output /tmp/goOmniPerf.json` 可重复运行独立进程并发与内存矩阵。 +- 已用 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 回收正常。 +- 自动验收入口:`python3 scripts/verifyPerformance.py`;Linux 固定验收入口:`python3 scripts/verifyLinux.py --load --tls --duration 30m`。真实部署仍需按目标代理和内核参数执行 30 分钟发布前压测。 以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL、适配层契约和一次业务模块试用已完成,自研网络层已有可运行预览,稳定性与生产规模验证尚未完成。 diff --git a/cmd/go-omni/README.md b/cmd/go-omni/README.md index b2615da..ff7c4a6 100644 --- a/cmd/go-omni/README.md +++ b/cmd/go-omni/README.md @@ -3,7 +3,7 @@ 从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架: ```sh -go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.5.0 +go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.6.0-preview go-omni new --module example.com/billing ./billing cd billing go mod tidy @@ -16,7 +16,7 @@ go run ./cmd/server 需要选择其他已发布版本时,使用完整版本号: ```sh -go-omni new --framework-version v0.5.0 --module example.com/billing ./billing +go-omni new --framework-version v0.6.0-preview --module example.com/billing ./billing ``` `--framework-version` 禁用自动本地替换;`main`、`latest` 等浮动引用不接受为固定版本。 @@ -48,8 +48,8 @@ MySQL 项目额外包含 `schema.sql`、MySQL Repository 和可选的数据库 发布验收使用 Python 3 标准库脚本,不需要额外 Python 依赖: ```sh -python3 scripts/verifyScaffold.py --version v0.5.0 -python3 scripts/verifyDatabase.py --version v0.5.0 +python3 scripts/verifyScaffold.py --version v0.6.0-preview +python3 scripts/verifyDatabase.py --version v0.6.0-preview ``` 脚本在源码目录外安装远程 CLI、生成项目,确认固定版本且没有 `replace`,再下载依赖、 diff --git a/cmd/go-omni/scaffold/version.go b/cmd/go-omni/scaffold/version.go index 04a942e..dbde2b9 100644 --- a/cmd/go-omni/scaffold/version.go +++ b/cmd/go-omni/scaffold/version.go @@ -10,7 +10,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni" // Source builds can carry an unpublished VCS version (including +dirty). // Keep this fallback in sync with the next release; installed CLIs use their // downloaded module version, which also has a module checksum. -const defaultFrameworkVersion = "v0.5.0" +const defaultFrameworkVersion = "v0.6.0-preview" var frameworkVersionPattern = regexp.MustCompile(`^v[01]\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`) diff --git a/internal/perfcheck/main.go b/internal/perfcheck/main.go index f382c28..02156fa 100644 --- a/internal/perfcheck/main.go +++ b/internal/perfcheck/main.go @@ -36,40 +36,42 @@ import ( const expectedBody = `{"code":"OK","message":"success","data":{"status":"ok"},"requestId":"perfCheck"}` type snapshot struct { - Address string `json:"address,omitempty"` - HeapAlloc uint64 `json:"heapAlloc"` - HeapInuse uint64 `json:"heapInuse"` - StackInuse uint64 `json:"stackInuse"` - TotalAlloc uint64 `json:"totalAlloc"` - Mallocs uint64 `json:"mallocs"` - NumGC uint32 `json:"numGc"` - Goroutines int `json:"goroutines"` - Connections int64 `json:"connections"` - Accepted int64 `json:"accepted"` + Address string `json:"address,omitempty"` + HeapAlloc uint64 `json:"heapAlloc"` + HeapInuse uint64 `json:"heapInuse"` + StackInuse uint64 `json:"stackInuse"` + TotalAlloc uint64 `json:"totalAlloc"` + Mallocs uint64 `json:"mallocs"` + NumGC uint32 `json:"numGc"` + Goroutines int `json:"goroutines"` + Connections int64 `json:"connections"` + Accepted int64 `json:"accepted"` + FileDescriptors int `json:"fileDescriptors,omitempty"` } type result struct { - Adapter string `json:"adapter"` - Scenario string `json:"scenario"` - Concurrency int `json:"concurrency"` - Run int `json:"run"` - Requests uint64 `json:"requests,omitempty"` - Errors uint64 `json:"errors"` - FirstError string `json:"firstError,omitempty"` - ElapsedSeconds float64 `json:"elapsedSeconds,omitempty"` - RequestsPerSecond float64 `json:"requestsPerSecond,omitempty"` - P50Millis float64 `json:"p50Millis,omitempty"` - P95Millis float64 `json:"p95Millis,omitempty"` - P99Millis float64 `json:"p99Millis,omitempty"` - ServerBytesPerRequest float64 `json:"serverBytesPerRequest,omitempty"` - ServerAllocsPerRequest float64 `json:"serverAllocsPerRequest,omitempty"` - SampledPeakHeapAlloc uint64 `json:"sampledPeakHeapAlloc,omitempty"` - IdleHeapDelta int64 `json:"idleHeapDelta,omitempty"` - IdleHeapPerConnection float64 `json:"idleHeapPerConnection,omitempty"` - Before snapshot `json:"before"` - During snapshot `json:"during"` - After snapshot `json:"after"` - AfterReclaim snapshot `json:"afterReclaim"` + Adapter string `json:"adapter"` + Scenario string `json:"scenario"` + Concurrency int `json:"concurrency"` + Run int `json:"run"` + Requests uint64 `json:"requests,omitempty"` + Errors uint64 `json:"errors"` + FirstError string `json:"firstError,omitempty"` + ElapsedSeconds float64 `json:"elapsedSeconds,omitempty"` + RequestsPerSecond float64 `json:"requestsPerSecond,omitempty"` + P50Millis float64 `json:"p50Millis,omitempty"` + P95Millis float64 `json:"p95Millis,omitempty"` + P99Millis float64 `json:"p99Millis,omitempty"` + ServerBytesPerRequest float64 `json:"serverBytesPerRequest,omitempty"` + ServerAllocsPerRequest float64 `json:"serverAllocsPerRequest,omitempty"` + SampledPeakHeapAlloc uint64 `json:"sampledPeakHeapAlloc,omitempty"` + SampledPeakFileDescriptors int `json:"sampledPeakFileDescriptors,omitempty"` + IdleHeapDelta int64 `json:"idleHeapDelta,omitempty"` + IdleHeapPerConnection float64 `json:"idleHeapPerConnection,omitempty"` + Before snapshot `json:"before"` + During snapshot `json:"during"` + After snapshot `json:"after"` + AfterReclaim snapshot `json:"afterReclaim"` } type report struct { @@ -239,12 +241,21 @@ func serve(adapter string, tlsEnabled bool) error { StackInuse: memory.StackInuse, TotalAlloc: memory.TotalAlloc, Mallocs: memory.Mallocs, NumGC: memory.NumGC, Goroutines: runtime.NumGoroutine(), Connections: tracked.active.Load(), Accepted: tracked.accepted.Load(), + FileDescriptors: openFileDescriptorCount(), }); err != nil { return err } } } +func openFileDescriptorCount() int { + entries, err := os.ReadDir("/proc/self/fd") + if err != nil { + return 0 + } + return len(entries) +} + type child struct { cmd *exec.Cmd input io.WriteCloser @@ -265,8 +276,9 @@ func startChildMode(adapter string, tlsEnabled bool) (*child, error) { if err != nil { return nil, err } - // This bounds even a broken server or protocol read; it is not a load duration. - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + // This bounds even a broken server or protocol read; it is not a load + // duration. Keep it above the supported 30-minute soak plus drain time. + ctx, cancel := context.WithTimeout(context.Background(), 35*time.Minute) args := []string{"--server", adapter} if tlsEnabled { args = append(args, "--tls") @@ -451,6 +463,9 @@ func traffic(c *child, concurrency int, duration time.Duration) (result, error) } else if s.HeapAlloc > r.SampledPeakHeapAlloc { r.SampledPeakHeapAlloc = s.HeapAlloc } + if s.FileDescriptors > r.SampledPeakFileDescriptors { + r.SampledPeakFileDescriptors = s.FileDescriptors + } } } r.ElapsedSeconds = time.Since(begin).Seconds() @@ -472,6 +487,7 @@ func traffic(c *child, concurrency int, duration time.Duration) (result, error) return r, err } r.SampledPeakHeapAlloc = max(r.SampledPeakHeapAlloc, r.During.HeapAlloc) + r.SampledPeakFileDescriptors = max(r.SampledPeakFileDescriptors, r.During.FileDescriptors) if r.Requests == 0 { return r, errors.New("no requests completed") } @@ -532,6 +548,9 @@ func idle(c *child, count int) (result, error) { if r.During.Connections != int64(count) { return r, fmt.Errorf("idle connections: want %d, have %d", count, r.During.Connections) } + if r.During.FileDescriptors > 0 && r.Before.FileDescriptors > 0 && r.During.FileDescriptors < r.Before.FileDescriptors+count { + return r, fmt.Errorf("idle file descriptors: want at least %d, have %d", r.Before.FileDescriptors+count, r.During.FileDescriptors) + } r.IdleHeapDelta = int64(r.During.HeapAlloc) - int64(r.Before.HeapAlloc) r.IdleHeapPerConnection = float64(r.IdleHeapDelta) / float64(count) for _, conn := range connections { @@ -586,8 +605,8 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int if err != nil { return err } - if count < 1 || duration < 200*time.Millisecond || duration > 30*time.Second || len(concurrency)+len(idleLevels) == 0 { - return errors.New("count must be positive, duration 200ms..30s, and at least one scenario selected") + if count < 1 || duration < 200*time.Millisecond || duration > 30*time.Minute || len(concurrency)+len(idleLevels) == 0 { + return errors.New("count must be positive, duration 200ms..30m, and at least one scenario selected") } report := report{ CreatedAt: time.Now().UTC().Format(time.RFC3339), GoVersion: runtime.Version(), @@ -598,6 +617,7 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", "Latency percentiles include failures and are histogram upper bounds with <=6.25% rounding.", "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "FileDescriptors is read from /proc/self/fd on Linux and omitted on other platforms.", "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection.", @@ -638,7 +658,7 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int if scenario.name == "traffic" { fmt.Printf("%s concurrency=%d run=%d: %.0f req/s p95=%.3fms p99=%.3fms server=%.0f B/req errors=%d\n", adapter, level, run, value.RequestsPerSecond, value.P95Millis, value.P99Millis, value.ServerBytesPerRequest, value.Errors) } else { - fmt.Printf("%s idle=%d run=%d: server heap delta=%.2f MiB (%.0f B/conn), goroutines=%d->%d->%d errors=%d\n", adapter, level, run, float64(value.IdleHeapDelta)/(1<<20), value.IdleHeapPerConnection, value.Before.Goroutines, value.During.Goroutines, value.After.Goroutines, value.Errors) + fmt.Printf("%s idle=%d run=%d: server heap delta=%.2f MiB (%.0f B/conn), goroutines=%d->%d->%d, fds=%d->%d->%d errors=%d\n", adapter, level, run, float64(value.IdleHeapDelta)/(1<<20), value.IdleHeapPerConnection, value.Before.Goroutines, value.During.Goroutines, value.After.Goroutines, value.Before.FileDescriptors, value.During.FileDescriptors, value.After.FileDescriptors, value.Errors) } } } diff --git a/performance/linuxSoak.json b/performance/linuxSoak.json new file mode 100644 index 0000000..b4bcf15 --- /dev/null +++ b/performance/linuxSoak.json @@ -0,0 +1,265 @@ +{ + "createdAt": "2026-09-21T17:29:24Z", + "goVersion": "go1.25.14", + "platform": "linux/arm64", + "gomaxprocs": 4, + "duration": "30s", + "tls": false, + "notes": [ + "Loopback HTTP/1.1 health response, fixed requestId, optional TLS, no access logs or database.", + "Each case starts a fresh server process; load client runs in the parent process on the same machine.", + "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", + "Latency percentiles include failures and are histogram upper bounds with \u003c=6.25% rounding.", + "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "FileDescriptors is read from /proc/self/fd on Linux and omitted on other platforms.", + "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", + "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", + "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection." + ], + "results": [ + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 4456189, + "errors": 0, + "elapsedSeconds": 30.001393348, + "requestsPerSecond": 148532.7347403705, + "p50Millis": 0.589824, + "p95Millis": 2.62144, + "p99Millis": 3.538944, + "serverBytesPerRequest": 4427.911252417705, + "serverAllocsPerRequest": 47.07614668049313, + "sampledPeakHeapAlloc": 6219064, + "sampledPeakFileDescriptors": 135, + "before": { + "heapAlloc": 2105600, + "heapInuse": 4202496, + "stackInuse": 1015808, + "totalAlloc": 4172144, + "mallocs": 30855, + "numGc": 2, + "goroutines": 58, + "connections": 56, + "accepted": 56, + "fileDescriptors": 63 + }, + "during": { + "heapAlloc": 3500920, + "heapInuse": 5947392, + "stackInuse": 2097152, + "totalAlloc": 19735781560, + "mallocs": 209811062, + "numGc": 6570, + "goroutines": 130, + "connections": 128, + "accepted": 128, + "fileDescriptors": 135 + }, + "after": { + "heapAlloc": 2828640, + "heapInuse": 5054464, + "stackInuse": 1015808, + "totalAlloc": 19735811920, + "mallocs": 209811396, + "numGc": 6571, + "goroutines": 2, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1722584, + "heapInuse": 3530752, + "stackInuse": 1015808, + "totalAlloc": 19735822392, + "mallocs": 209811421, + "numGc": 6572, + "goroutines": 2, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 4398474, + "errors": 0, + "elapsedSeconds": 30.000375472, + "requestsPerSecond": 146613.96501871088, + "p50Millis": 0.622592, + "p95Millis": 2.62144, + "p99Millis": 3.670016, + "serverBytesPerRequest": 4591.297385411395, + "serverAllocsPerRequest": 59.0589788640333, + "sampledPeakHeapAlloc": 4980920, + "sampledPeakFileDescriptors": 135, + "before": { + "heapAlloc": 2202712, + "heapInuse": 4120576, + "stackInuse": 1671168, + "totalAlloc": 4300920, + "mallocs": 32647, + "numGc": 2, + "goroutines": 131, + "connections": 128, + "accepted": 128, + "fileDescriptors": 135 + }, + "during": { + "heapAlloc": 4382752, + "heapInuse": 6021120, + "stackInuse": 1638400, + "totalAlloc": 20199003096, + "mallocs": 259802030, + "numGc": 9032, + "goroutines": 131, + "connections": 128, + "accepted": 128, + "fileDescriptors": 135 + }, + "after": { + "heapAlloc": 2260152, + "heapInuse": 4382720, + "stackInuse": 557056, + "totalAlloc": 20199025048, + "mallocs": 259802362, + "numGc": 9033, + "goroutines": 3, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1645880, + "heapInuse": 3489792, + "stackInuse": 557056, + "totalAlloc": 20199035520, + "mallocs": 259802387, + "numGc": 9034, + "goroutines": 3, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 512, + "run": 1, + "errors": 0, + "idleHeapDelta": 5162344, + "idleHeapPerConnection": 10082.703125, + "before": { + "heapAlloc": 1516408, + "heapInuse": 3301376, + "stackInuse": 458752, + "totalAlloc": 3006544, + "mallocs": 23238, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0, + "fileDescriptors": 7 + }, + "during": { + "heapAlloc": 6678752, + "heapInuse": 8675328, + "stackInuse": 4718592, + "totalAlloc": 10467200, + "mallocs": 59319, + "numGc": 4, + "goroutines": 514, + "connections": 512, + "accepted": 512, + "fileDescriptors": 519 + }, + "after": { + "heapAlloc": 6108264, + "heapInuse": 8069120, + "stackInuse": 491520, + "totalAlloc": 10563168, + "mallocs": 60468, + "numGc": 5, + "goroutines": 2, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1806072, + "heapInuse": 3629056, + "stackInuse": 491520, + "totalAlloc": 10573944, + "mallocs": 60497, + "numGc": 6, + "goroutines": 2, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 512, + "run": 1, + "errors": 0, + "idleHeapDelta": 2692536, + "idleHeapPerConnection": 5258.859375, + "before": { + "heapAlloc": 1511104, + "heapInuse": 3309568, + "stackInuse": 425984, + "totalAlloc": 3001496, + "mallocs": 23227, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0, + "fileDescriptors": 7 + }, + "during": { + "heapAlloc": 4203640, + "heapInuse": 6283264, + "stackInuse": 4718592, + "totalAlloc": 8087336, + "mallocs": 58784, + "numGc": 4, + "goroutines": 515, + "connections": 512, + "accepted": 512, + "fileDescriptors": 519 + }, + "after": { + "heapAlloc": 4233984, + "heapInuse": 6316032, + "stackInuse": 458752, + "totalAlloc": 8182368, + "mallocs": 60013, + "numGc": 5, + "goroutines": 3, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1804784, + "heapInuse": 3686400, + "stackInuse": 458752, + "totalAlloc": 8193144, + "mallocs": 60042, + "numGc": 6, + "goroutines": 3, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + } + } + ] +} diff --git a/performance/linuxSoakTLS.json b/performance/linuxSoakTLS.json new file mode 100644 index 0000000..32fb58a --- /dev/null +++ b/performance/linuxSoakTLS.json @@ -0,0 +1,265 @@ +{ + "createdAt": "2026-09-21T17:27:47Z", + "goVersion": "go1.25.14", + "platform": "linux/arm64", + "gomaxprocs": 4, + "duration": "30s", + "tls": true, + "notes": [ + "Loopback HTTP/1.1 health response, fixed requestId, optional TLS, no access logs or database.", + "Each case starts a fresh server process; load client runs in the parent process on the same machine.", + "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", + "Latency percentiles include failures and are histogram upper bounds with \u003c=6.25% rounding.", + "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "FileDescriptors is read from /proc/self/fd on Linux and omitted on other platforms.", + "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", + "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", + "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection." + ], + "results": [ + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 4442303, + "errors": 0, + "elapsedSeconds": 30.001232972, + "requestsPerSecond": 148070.68109987277, + "p50Millis": 0.589824, + "p95Millis": 2.62144, + "p99Millis": 3.670016, + "serverBytesPerRequest": 4475.325703807237, + "serverAllocsPerRequest": 49.07248492504901, + "sampledPeakHeapAlloc": 7609472, + "sampledPeakFileDescriptors": 135, + "before": { + "heapAlloc": 3340488, + "heapInuse": 5988352, + "stackInuse": 2195456, + "totalAlloc": 9430136, + "mallocs": 83639, + "numGc": 5, + "goroutines": 124, + "connections": 122, + "accepted": 122, + "fileDescriptors": 135 + }, + "during": { + "heapAlloc": 6485192, + "heapInuse": 8716288, + "stackInuse": 1998848, + "totalAlloc": 19890182936, + "mallocs": 218078486, + "numGc": 5603, + "goroutines": 130, + "connections": 128, + "accepted": 128, + "fileDescriptors": 135 + }, + "after": { + "heapAlloc": 2853976, + "heapInuse": 5251072, + "stackInuse": 884736, + "totalAlloc": 19890213312, + "mallocs": 218078820, + "numGc": 5604, + "goroutines": 2, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1745496, + "heapInuse": 3686400, + "stackInuse": 884736, + "totalAlloc": 19890215312, + "mallocs": 218078842, + "numGc": 5605, + "goroutines": 2, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 4491731, + "errors": 0, + "elapsedSeconds": 30.000919805, + "requestsPerSecond": 149719.77623337405, + "p50Millis": 0.622592, + "p95Millis": 2.62144, + "p99Millis": 3.538944, + "serverBytesPerRequest": 4807.806184297323, + "serverAllocsPerRequest": 61.06011045630293, + "sampledPeakHeapAlloc": 5859960, + "sampledPeakFileDescriptors": 135, + "before": { + "heapAlloc": 2685024, + "heapInuse": 5136384, + "stackInuse": 2621440, + "totalAlloc": 8854080, + "mallocs": 83570, + "numGc": 5, + "goroutines": 125, + "connections": 122, + "accepted": 122, + "fileDescriptors": 135 + }, + "during": { + "heapAlloc": 3345480, + "heapInuse": 5980160, + "stackInuse": 1605632, + "totalAlloc": 21604226160, + "mallocs": 274349161, + "numGc": 7802, + "goroutines": 131, + "connections": 128, + "accepted": 128, + "fileDescriptors": 135 + }, + "after": { + "heapAlloc": 2764288, + "heapInuse": 5341184, + "stackInuse": 1605632, + "totalAlloc": 21604248208, + "mallocs": 274349494, + "numGc": 7803, + "goroutines": 3, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1654056, + "heapInuse": 3506176, + "stackInuse": 1605632, + "totalAlloc": 21604258680, + "mallocs": 274349519, + "numGc": 7804, + "goroutines": 3, + "connections": 0, + "accepted": 128, + "fileDescriptors": 7 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 512, + "run": 1, + "errors": 0, + "idleHeapDelta": 7274360, + "idleHeapPerConnection": 14207.734375, + "before": { + "heapAlloc": 1515704, + "heapInuse": 3301376, + "stackInuse": 425984, + "totalAlloc": 3023896, + "mallocs": 23663, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0, + "fileDescriptors": 7 + }, + "during": { + "heapAlloc": 8790064, + "heapInuse": 12066816, + "stackInuse": 4718592, + "totalAlloc": 29570320, + "mallocs": 272972, + "numGc": 9, + "goroutines": 514, + "connections": 512, + "accepted": 512, + "fileDescriptors": 519 + }, + "after": { + "heapAlloc": 6138608, + "heapInuse": 8765440, + "stackInuse": 458752, + "totalAlloc": 29675376, + "mallocs": 274141, + "numGc": 10, + "goroutines": 2, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1820824, + "heapInuse": 3825664, + "stackInuse": 458752, + "totalAlloc": 29677376, + "mallocs": 274163, + "numGc": 11, + "goroutines": 2, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 512, + "run": 1, + "errors": 0, + "idleHeapDelta": 4689576, + "idleHeapPerConnection": 9159.328125, + "before": { + "heapAlloc": 1520864, + "heapInuse": 3284992, + "stackInuse": 458752, + "totalAlloc": 3029160, + "mallocs": 23661, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0, + "fileDescriptors": 7 + }, + "during": { + "heapAlloc": 6210440, + "heapInuse": 9314304, + "stackInuse": 8880128, + "totalAlloc": 27150344, + "mallocs": 271905, + "numGc": 10, + "goroutines": 515, + "connections": 512, + "accepted": 512, + "fileDescriptors": 519 + }, + "after": { + "heapAlloc": 6230688, + "heapInuse": 9330688, + "stackInuse": 491520, + "totalAlloc": 27245184, + "mallocs": 273078, + "numGc": 11, + "goroutines": 3, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + }, + "afterReclaim": { + "heapAlloc": 1817296, + "heapInuse": 3702784, + "stackInuse": 491520, + "totalAlloc": 27255960, + "mallocs": 273107, + "numGc": 12, + "goroutines": 3, + "connections": 0, + "accepted": 512, + "fileDescriptors": 7 + } + } + ] +} diff --git a/scripts/verifyBusiness.py b/scripts/verifyBusiness.py index 1234f18..f53e6d0 100755 --- a/scripts/verifyBusiness.py +++ b/scripts/verifyBusiness.py @@ -25,7 +25,7 @@ def free_address(): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") - parser.add_argument("--version", default="v0.5.0", help="Target framework release") + parser.add_argument("--version", default="v0.6.0-preview", help="Target framework release") parser.add_argument("--from-version", default="v0.2.0", help="Version used before upgrade") args = parser.parse_args() diff --git a/scripts/verifyDatabase.py b/scripts/verifyDatabase.py index 0c4b8b8..b15933f 100644 --- a/scripts/verifyDatabase.py +++ b/scripts/verifyDatabase.py @@ -145,7 +145,7 @@ class Server: def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") - parser.add_argument("--version", default="v0.5.0") + parser.add_argument("--version", default="v0.6.0-preview") parser.add_argument("--cli", help="Use a locally built CLI") parser.add_argument("--framework-dir", help="Local framework source for pre-release checks") parser.add_argument("--image", default="mysql:8.4") diff --git a/scripts/verifyLinux.py b/scripts/verifyLinux.py new file mode 100755 index 0000000..c311c0a --- /dev/null +++ b/scripts/verifyLinux.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Run GoOmni's repeatable Linux checks inside a pinned Go Docker image.""" + +import argparse +from pathlib import Path +import shutil +import shlex +import subprocess +import sys + + +def workspace_output(root: Path, value: str) -> tuple[Path, str]: + requested = Path(value) + host_path = requested if requested.is_absolute() else root / requested + host_path = host_path.resolve() + try: + relative = host_path.relative_to(root) + except ValueError as exc: + raise SystemExit("--output must be inside the repository") from exc + host_path.parent.mkdir(parents=True, exist_ok=True) + return host_path, "/workspace/" + relative.as_posix() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--image", default="golang:1.25", help="Docker image (default: golang:1.25)") + parser.add_argument("--gomaxprocs", type=int, default=4) + parser.add_argument("--load", action="store_true", help="also run the isolated server matrix") + parser.add_argument("--tls", action="store_true", help="run the load matrix over TLS") + parser.add_argument("--duration", default="2s", help="duration per traffic case (up to 30m)") + parser.add_argument("--count", type=int, default=3) + parser.add_argument("--concurrency", default="1,32,128") + parser.add_argument("--idle", default="128,1024") + parser.add_argument("--output", help="JSON report path inside the repository") + args = parser.parse_args() + if args.gomaxprocs < 1 or args.count < 1: + parser.error("--gomaxprocs and --count must be positive") + if args.tls and not args.load: + parser.error("--tls requires --load") + + root = Path(__file__).resolve().parent.parent + docker = shutil.which("docker") + if docker is None: + raise SystemExit("docker is required for Linux verification") + + commands = [ + "go test -race ./...", + "go vet ./...", + "go build ./...", + ] + if args.load: + load = [ + "go", "run", "./internal/perfcheck", + "--duration", args.duration, + "--count", str(args.count), + "--concurrency", args.concurrency, + "--idle", args.idle, + ] + if args.tls: + load.append("--tls") + if args.output: + _, container_path = workspace_output(root, args.output) + load += ["--output", container_path] + commands.append(" ".join(shlex.quote(part) for part in load)) + + command = " && ".join(commands) + docker_args = [ + docker, "run", "--rm", "--pull=missing", + "-v", f"{root}:/workspace", + "-w", "/workspace", + "-e", "GOWORK=off", + "-e", f"GOMAXPROCS={args.gomaxprocs}", + args.image, "sh", "-c", command, + ] + print(f"Linux verification image: {args.image}", flush=True) + print(f"Linux verification command: {command}", flush=True) + result = subprocess.run(docker_args, cwd=root) + if result.returncode != 0: + raise SystemExit(f"Linux verification failed with exit code {result.returncode}") + print("PASS Linux verification", flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/verifyPerformance.py b/scripts/verifyPerformance.py index 56a7dd1..747e67e 100755 --- a/scripts/verifyPerformance.py +++ b/scripts/verifyPerformance.py @@ -14,6 +14,8 @@ def main(): parser.add_argument("--load", action="store_true", help="Also compare isolated servers and idle connection memory") parser.add_argument("--tls", action="store_true", help="Run the isolated load matrix over TLS") parser.add_argument("--duration", default="2s", help="Duration per load scenario") + parser.add_argument("--concurrency", default="1,32,128") + parser.add_argument("--idle", default="128,1024") parser.add_argument("--output", help="JSON output path for --load") args = parser.parse_args() if args.count < 1: @@ -32,7 +34,8 @@ def main(): ] if args.load: command = [args.go, "run", "./internal/perfcheck", "--duration", args.duration, - "--count", str(args.count)] + "--count", str(args.count), "--concurrency", args.concurrency, + "--idle", args.idle] if args.tls: command.append("--tls") if args.output: diff --git a/scripts/verifyScaffold.py b/scripts/verifyScaffold.py index f410725..f87a0b5 100644 --- a/scripts/verifyScaffold.py +++ b/scripts/verifyScaffold.py @@ -91,7 +91,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go", help="Go executable") - parser.add_argument("--version", default="v0.5.0", help="Released CLI version to install") + parser.add_argument("--version", default="v0.6.0-preview", help="Released CLI version to install") parser.add_argument("--cli", help="Use a locally built CLI instead of installing") parser.add_argument("--framework-version", help="Explicit version passed to the CLI") parser.add_argument("--framework-dir", help="Local checkout for pre-release verification") diff --git a/scripts/verifyTransports.py b/scripts/verifyTransports.py index 56df357..be07d06 100644 --- a/scripts/verifyTransports.py +++ b/scripts/verifyTransports.py @@ -13,7 +13,7 @@ from verifyScaffold import MODULE, run def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") - parser.add_argument("--version", default="v0.5.0") + parser.add_argument("--version", default="v0.6.0-preview") parser.add_argument("--framework-dir", help="Use a local checkout before release") args = parser.parse_args() env = dict(os.environ, GOWORK="off") diff --git a/transport/README.md b/transport/README.md index ff3edd7..6d2249a 100644 --- a/transport/README.md +++ b/transport/README.md @@ -103,7 +103,7 @@ func TestContract(t *testing.T) { ```sh go test -race ./transport/... ./examples/reference -python3 scripts/verifyTransports.py --version v0.5.0 +python3 scripts/verifyTransports.py --version v0.6.0-preview ``` 第一条验证标准库适配器复用完整 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。 diff --git a/transport/native/server_test.go b/transport/native/server_test.go index f41b398..2ce3edd 100644 --- a/transport/native/server_test.go +++ b/transport/native/server_test.go @@ -749,7 +749,7 @@ func TestServerConnectionChurnReusesStateSafely(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) server := native.New(application) - server.MaxConnections = 16 + server.MaxConnections = 32 listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) @@ -759,8 +759,10 @@ func TestServerConnectionChurnReusesStateSafely(t *testing.T) { waitForServer(t, listener.Addr().String()) defer stopServer(t, server, listener, done) - const workers = 8 - const requestsPerWorker = 100 + // Repeated short-lived connections exercise accept/close, connection-pool + // reuse and the MaxConnections gate under a bursty workload. + const workers = 16 + const requestsPerWorker = 200 errors := make(chan error, workers) var group sync.WaitGroup for worker := 0; worker < workers; worker++ { diff --git a/transport/native/slow_client_test.go b/transport/native/slow_client_test.go new file mode 100644 index 0000000..5ab88fa --- /dev/null +++ b/transport/native/slow_client_test.go @@ -0,0 +1,72 @@ +package native_test + +import ( + "context" + "io" + "net" + "net/http" + "sync/atomic" + "testing" + "time" + + "git.yuntuo.hk/k2safe/go-omni/transport/native" +) + +func TestServerSlowHeadersCloseBeforeHandler(t *testing.T) { + var handlerCalls atomic.Int32 + server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + handlerCalls.Add(1) + })) + server.ReadHeaderTimeout = 40 * time.Millisecond + 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) }() + defer stopServer(t, server, listener, done) + waitForServer(t, listener.Addr().String()) + + conn, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + if _, err := io.WriteString(conn, "GET /health HTTP/1.1\r\nHost: local"); err != nil { + t.Fatal(err) + } + requirePeerClose(t, conn) + if handlerCalls.Load() != 0 { + t.Fatal("handler ran before slow headers completed") + } +} + +func TestServerSlowChunkedBodyClosesBeforeHandler(t *testing.T) { + var handlerCalls atomic.Int32 + server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + handlerCalls.Add(1) + })) + server.ReadTimeout = 40 * time.Millisecond + 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) }() + defer stopServer(t, server, listener, done) + waitForServer(t, listener.Addr().String()) + + conn, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + request := "POST /body HTTP/1.1\r\nHost: local\r\nTransfer-Encoding: chunked\r\n\r\n4\r\npa" + if _, err := io.WriteString(conn, request); err != nil { + t.Fatal(err) + } + requirePeerClose(t, conn) + if handlerCalls.Load() != 0 { + t.Fatal("handler ran before slow chunked body completed") + } +}