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

@@ -1,6 +1,6 @@
# go-omni CLI
v0.26 新增 `i18n check|export|types|extract`:检查独立语言资源、提取 Go 静态消息引用或已导出的错误目录,导出指定命名空间的前端包和 TypeScript 类型。输入只读,结果写 stdout;动态引用明确报告。完整命令见 [国际化工具链](../../i18n/README.md#cli-提取检查与导出)。`.api errorCatalog` 可声明 translation 引用,旧 runtime 在生成前拒绝,接入时使用同版 v0.26 CLI 与框架。
v0.26 新增 `i18n check|export|types|extract`:检查独立语言资源、提取 Go 静态消息引用或已导出的错误目录,导出指定命名空间的前端包和 TypeScript 类型。输入只读,结果写 stdout;动态引用明确报告。完整命令见 [国际化工具链](../../i18n/README.md#cli-提取检查与导出)。`.api errorCatalog` 可声明 translation 引用,旧 runtime 在生成前拒绝,接入时使用当前同版 CLI 与框架。
v0.19 移除结构映射生成:`omni.gen.json` 只保留 Repository 和中间件选项。普通接口直接使用生成 DTO;需要类型转换时用普通 Go 或可选 [copyx](../../COPY.md)。旧 `mappings` / `mappingFiles` 必须按迁移步骤清理,CLI 不会静默删除仍在调用的旧生成文件。
@@ -16,7 +16,7 @@ v0.14 新增 `api format [--check]`、`api upgrade [--dry-run|--preview <空目
```sh
# 仓库读取权限与 PATH 配置见根目录 README
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.26.0-preview
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.27.0-preview
go-omni new --module example.com/hello ./hello
# 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在
@@ -51,14 +51,14 @@ main、Gen.go、OpenAPI 和 manifest 由生成器维护;业务文件按字节
## 版本与迁移
CLI 与框架依赖固定使用 v0.26.0-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换:
CLI 与框架依赖固定使用 v0.27.0-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换:
```sh
# 在 go-omni 源码仓库中执行
go run ./cmd/go-omni new --module example.com/hello --framework-dir . ../hello
```
当前模板与依赖以 v0.26.0-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
当前模板与依赖以 v0.27.0-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
## 数据库迁移命令

View File

@@ -22,7 +22,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
// Runtime-only releases need not raise the managed template compatibility floor.
const minimumVersion = "v0.22.0-preview"
const currentVersion = "v0.26.0-preview"
const currentVersion = "v0.27.0-preview"
const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n"
type Options struct {

View File

@@ -30,12 +30,12 @@ func main(){
func run(ctx context.Context,path string)(runErr error){
cfg,err:=config.Load(path);if err!=nil{return err}
logger,err:=logx.New(cfg.Server.Log);if err!=nil{return err}
// Unjoined background work may still log. The owning process exits on this
// Unjoined HTTP or background work may still log. The owning process exits on this
// error; do not close its borrowed logger before that work has finished.
defer func(){if !errors.Is(runErr,lifecycle.ErrResourcesRetained){runErr=errors.Join(runErr,logger.Close())}}()
deps,err:=svc.New(ctx,logger,cfg);if err!=nil{return err}
// RunContext drains HTTP requests before returning. This scope owns dependencies
// and closes them exactly once, before closing the borrowed logger.
// Finish propagates incomplete HTTP draining: stop background work but retain
// shared dependencies when any handler is still running. Otherwise close once.
defer func(){runErr=deps.Finish(runErr)}()
app,err:=handler.NewApp(deps);if err!=nil{return err}
defer func(){runErr=errors.Join(runErr,app.Close())}()

View File

@@ -7,6 +7,7 @@ import (
)
var Opened, Closed, DownloadClosed atomic.Int32
var SlowReturned atomic.Int32
var Started = make(chan struct{}, 1)
var Release = make(chan struct{})
var Current atomic.Pointer[ServiceContext]

View File

@@ -71,6 +71,7 @@ type countedReader struct{ io.Reader }
func (c *countedReader) Close() error { svc.DownloadClosed.Add(1); return nil }
func (l *Logic) Slow(context.Context) (*types.Receipt, error) {
defer svc.SlowReturned.Add(1)
before := svc.Closed.Load()
svc.Started <- struct{}{}
<-svc.Release

View File

@@ -19,6 +19,7 @@ import (
"example.com/managed/internal/handler"
"example.com/managed/internal/svc"
"git.yuntuo.hk/k2safe/go-omni/health"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
@@ -229,3 +230,80 @@ func TestGeneratedEndpointConflictCleansUp(t *testing.T) {
t.Fatal("collision leaked resources or started serving", err)
}
}
func TestGeneratedIncompleteHTTPDrainRetainsDependencies(t *testing.T) {
for _, transport := range []string{"nethttp", "native"} {
t.Run(transport, func(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
address := l.Addr().String()
l.Close()
path := filepath.Join(t.TempDir(), "config.json")
logFile := filepath.Join(t.TempDir(), "service.log")
data, _ := json.Marshal(map[string]any{"server": map[string]any{"address": address, "transport": transport, "shutdownTimeout": "30ms", "http": map[string]string{"shutdownCancelTimeout": "30ms"}, "log": map[string]any{"file": logFile, "rotation": map[string]any{"enabled": true}}}})
if err = os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
svc.Started = make(chan struct{}, 1)
svc.Release = make(chan struct{})
before := svc.Closed.Load()
returned := svc.SlowReturned.Load()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan error, 1)
go func() { done <- run(ctx, path) }()
waitReady(t, "http://"+address, done)
client := &http.Client{Timeout: 3 * time.Second}
defer client.CloseIdleConnections()
requestDone := make(chan struct{})
go func() {
defer close(requestDone)
res, err := client.Get("http://" + address + "/slow")
if err == nil {
io.Copy(io.Discard, res.Body)
res.Body.Close()
}
}()
await(t, svc.Started)
// Capture the channel, because a later test installs its own fixture.
release := svc.Release
deps := svc.Current.Load()
defer func() {
close(release)
deadline := time.Now().Add(3 * time.Second)
for svc.SlowReturned.Load() == returned {
if time.Now().After(deadline) {
t.Error("handler did not exit after test release")
break
}
time.Sleep(time.Millisecond)
}
// The test owns the controlled handler and has now joined it.
_ = deps.Logger.Close()
}()
cancel()
select {
case err = <-done:
if !errors.Is(err, lifecycle.ErrResourcesRetained) || !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("unbounded HTTP shutdown")
}
if err = deps.Close(); !errors.Is(err, lifecycle.ErrResourcesRetained) {
t.Fatal("deferred Close bypassed retention", err)
}
if svc.Closed.Load() != before {
t.Fatal("generated main closed client during active request")
}
deps.Logger.Info("retained.http.logger")
logs, err := os.ReadFile(logFile)
if err != nil || !strings.Contains(string(logs), "retained.http.logger") {
t.Fatal("generated main closed logger during active request", err, string(logs))
}
await(t, requestDone)
})
}
}

View File

@@ -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.26.0-preview"
const defaultFrameworkVersion = "v0.27.0-preview"
// This target is not a claim that the release already exists. Source users
// select a local framework checkout until the module templates are published.