release: prepare v0.6.0-preview
This commit is contained in:
10
CHANGELOG.md
10
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
|
||||
|
||||
|
||||
@@ -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 决定是否优化对象分配或事件状态机。
|
||||
|
||||
@@ -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` 可测量独立服务进程并保存报告。
|
||||
|
||||
|
||||
11
ROADMAP.md
11
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、适配层契约和一次业务模块试用已完成,自研网络层已有可运行预览,稳定性与生产规模验证尚未完成。
|
||||
|
||||
@@ -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`,再下载依赖、
|
||||
|
||||
@@ -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-]+)*)?$`)
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ type snapshot struct {
|
||||
Goroutines int `json:"goroutines"`
|
||||
Connections int64 `json:"connections"`
|
||||
Accepted int64 `json:"accepted"`
|
||||
FileDescriptors int `json:"fileDescriptors,omitempty"`
|
||||
}
|
||||
|
||||
type result struct {
|
||||
@@ -64,6 +65,7 @@ type result struct {
|
||||
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"`
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
265
performance/linuxSoak.json
Normal file
265
performance/linuxSoak.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
265
performance/linuxSoakTLS.json
Normal file
265
performance/linuxSoakTLS.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
84
scripts/verifyLinux.py
Executable file
84
scripts/verifyLinux.py
Executable file
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
72
transport/native/slow_client_test.go
Normal file
72
transport/native/slow_client_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user