feat: release v0.27 unified HTTP limits and safe request shutdown
This commit is contained in:
15
examples/httpSafety/README.md
Normal file
15
examples/httpSafety/README.md
Normal 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。
|
||||
62
examples/httpSafety/main.go
Normal file
62
examples/httpSafety/main.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user