feat: add dependency shutdown and configurable service endpoints

This commit is contained in:
2026-09-24 18:33:24 +08:00
parent 8a348951eb
commit f0991d3ff2
51 changed files with 1433 additions and 73 deletions

View File

@@ -6,12 +6,14 @@ import (
omni "git.yuntuo.hk/k2safe/go-omni"
"example.com/managed/internal/svc"
"git.yuntuo.hk/k2safe/go-omni/config"
"git.yuntuo.hk/k2safe/go-omni/health"
)
// NewApp is generated HTTP composition. The caller owns deps and its logger.
func NewApp(deps *svc.ServiceContext) (*omni.App, error) {
if deps == nil || deps.Health == nil {
return nil, fmt.Errorf("service context and health checker required")
if deps == nil || deps.Lifecycle == nil {
return nil, fmt.Errorf("service context and lifecycle controller required; see PROJECT.md upgrade instructions")
}
cfg := deps.Config.Server
for _, option := range deps.AppOptions() {
@@ -24,13 +26,42 @@ func NewApp(deps *svc.ServiceContext) (*omni.App, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}
if err := cfg.Endpoints.ValidateRoutes([]config.EndpointRoute{
{Method: "get", Path: "/check"},
{Method: "get", Path: "/other"},
{Method: "get", Path: "/raw"},
{Method: "post", Path: "/upload"},
{Method: "post", Path: "/callback"},
{Method: "get", Path: "/download"},
{Method: "get", Path: "/slow"},
}...); err != nil {
return nil, err
}
if (cfg.Endpoints.Liveness.Enabled || cfg.Endpoints.Readiness.Enabled) && deps.Health == nil {
return nil, fmt.Errorf("enabled health endpoints require a checker")
}
app := omni.New(omni.WithConfig(cfg))
app.Use(deps.GlobalMiddleware()...)
if err := RegisterRoutes(app, deps); err != nil {
_ = app.Close()
return nil, err
}
app.GET("/openapi.json", OpenAPI)
app.GET("/health/live", deps.Health.LivenessHandler())
app.GET("/health/ready", deps.Health.ReadinessHandler())
if cfg.Endpoints.OpenAPI.Enabled {
app.GET(cfg.Endpoints.OpenAPI.Path, OpenAPI)
}
if cfg.Endpoints.Liveness.Enabled {
var options []health.HandlerOption
if cfg.Endpoints.Liveness.ResponseMode == "raw" {
options = append(options, health.WithRawResponse())
}
app.GET(cfg.Endpoints.Liveness.Path, deps.Health.LivenessHandler(options...))
}
if cfg.Endpoints.Readiness.Enabled {
options := []health.HandlerOption{health.WithShutdownContext(deps.Lifecycle.Context())}
if cfg.Endpoints.Readiness.ResponseMode == "raw" {
options = append(options, health.WithRawResponse())
}
app.GET(cfg.Endpoints.Readiness.Path, deps.Health.ReadinessHandler(options...))
}
return app, nil
}

View File

@@ -8,6 +8,9 @@ func (c *ServiceContext) Close() error {
return nil
}
c.closeOnce.Do(func() {
if c.Lifecycle != nil {
c.Lifecycle.RequestShutdown(nil)
}
c.closeMu.Lock()
c.closed = true
cleanups := c.cleanups

View File

@@ -9,12 +9,14 @@ import (
"example.com/managed/internal/middleware"
"git.yuntuo.hk/k2safe/go-omni/health"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
// Context owns infrastructure, while Logger is borrowed from main.
// Add business repositories here; HTTP contracts do not define database tables.
type ServiceContext struct {
Lifecycle *lifecycle.Controller
Config config.Config
cleanups []func() error
Logger *logx.Logger
@@ -34,7 +36,7 @@ func New(ctx context.Context, logger *logx.Logger, configs ...config.Config) (re
ctx = context.Background()
}
if err = ctx.Err(); err != nil {
return nil, err
return nil, errors.Join(err, lifecycle.Cause(ctx))
}
if len(configs) > 1 {
return nil, errors.New("expected at most one configuration")
@@ -48,12 +50,18 @@ func New(ctx context.Context, logger *logx.Logger, configs ...config.Config) (re
if err != nil {
return nil, err
}
c := &ServiceContext{Config: cfg, Logger: logger, Health: checker, Middlewares: middleware.Defaults()}
c := &ServiceContext{Config: cfg, Logger: logger, Health: checker, Middlewares: middleware.Defaults(), Lifecycle: lifecycle.New(ctx)}
ctx = c.Lifecycle.Context()
defer func() {
if err != nil {
err = errors.Join(err, c.Close())
if cause := c.Lifecycle.Err(); cause != nil && !errors.Is(err, cause) {
err = errors.Join(err, cause)
}
}
}()
return c, nil
}
func (c *ServiceContext) RequestShutdown(err error) { c.Lifecycle.RequestShutdown(err) }