feat: release v0.27 unified HTTP limits and safe request shutdown

This commit is contained in:
2026-09-29 23:24:19 +08:00
parent 859f1851b1
commit 3b00bb55d1
42 changed files with 1191 additions and 70 deletions

View File

@@ -0,0 +1,15 @@
# HTTP 限额与停机
从仓库根目录运行 `go run ./examples/httpSafety`,可用 `-addr 127.0.0.1:18080` 指定端口。仅使用本机 HTTP 和模拟收尾,不连接数据库或其他服务。
上传验证:
```sh
dd if=/dev/zero bs=1048576 count=9 2>/dev/null | curl --data-binary @- http://127.0.0.1:8080/upload
```
普通路由默认 1 MiB,上传路由允许 16 MiB,服务硬上限 20 MiB;以上请求返回已读取的 9437184 字节。将上传路由改为 21 MiB 后,启动会报告该路由与服务硬上限冲突。实际文件上传须额外预留 multipart 字段/边界开销,并配置文件大小限制。
停机验证:另一个终端执行 `curl http://127.0.0.1:8080/work`,随后在服务终端按 Ctrl-C。先等待 1s 排空,再取消请求,处理器模拟 100ms 收尾;日志中先出现 `request cleanup completed`,再出现资源关闭记录。因为用尽排空预算,本次退出仍返回超时错误。
若处理器不在取消收尾窗口内退出,App 和 `deps.Finish(runErr)` 返回 `lifecycle.ErrResourcesRetained` 并跳过资源关闭。真实项目拥有的日志也必须在该错误下保留,生成入口已处理。进程应非零退出;库不自行退出进程,也不强杀 goroutine。

View File

@@ -0,0 +1,62 @@
// This example uses only local HTTP and simulated work; no external services.
package main
import (
"context"
"errors"
"flag"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
)
func main() {
address := flag.String("addr", "127.0.0.1:8080", "local listen address")
flag.Parse()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, *address); err != nil {
log.Print(err)
os.Exit(1)
}
}
func run(ctx context.Context, address string) (runErr error) {
deps := lifecycle.NewRuntime(ctx)
deps.OnClose(func() error { log.Print("shared resource closed after requests completed"); return nil })
defer func() { runErr = deps.Finish(runErr) }()
app := omni.New(omni.WithHTTPConfig(omni.HTTPConfig{
DefaultMaxBodyBytes: 1 << 20,
MaxBodyBytes: 20 << 20,
ShutdownCancelTimeout: time.Second,
}), omni.WithShutdownTimeout(time.Second))
defer func() { runErr = errors.Join(runErr, app.Close()) }()
app.HandleWithBodyLimit("POST", "/upload", 16<<20, nil, func(c *omni.Context) error {
n, err := io.Copy(io.Discard, c.Request().Body)
if err != nil {
return err
}
return c.OK(map[string]int64{"bytes": n})
})
app.GET("/work", func(c *omni.Context) error {
log.Print("request accepted")
select {
case <-time.After(10 * time.Second):
return c.OK("finished")
case <-c.Context().Done():
// Simulate bounded rollback/cleanup after cancellation. Dependencies
// remain available until this handler and its defers have returned.
time.Sleep(100 * time.Millisecond)
log.Print("request cleanup completed")
return c.Context().Err()
}
})
log.Printf("listening on %s; POST /upload or GET /work, then Ctrl-C", address)
return app.RunContext(deps.Context(), address)
}