feat: add dependency shutdown and configurable service endpoints
This commit is contained in:
@@ -10,14 +10,14 @@
|
||||
"entry": "api/managed.api",
|
||||
"generated": {
|
||||
"docs/openapi.json": "9eb707aefeefd3f4881c774534e2d143949c0edb1d8a4aabd762f978f6203df1",
|
||||
"internal/handler/appGen.go": "dbadfb73165e0e6ef10e8277dd1b8a0edc2992dac3690d634808ac92829c2654",
|
||||
"internal/handler/appGen.go": "c6f078932e1a598a46faebba397f4e753db9d8f4f29a02f9b6077a0557d6c14e",
|
||||
"internal/handler/demo/handlersGen.go": "7a6f9d5e105e44b07196cc540cee3f0562d5f3d1c5f0bc094434db1064d0339c",
|
||||
"internal/handler/openapiGen.go": "612920ed25e5dc7a4f673be56041d47b70aa3216707ff282e52dfe503c2d705a",
|
||||
"internal/handler/responseGen.go": "84397a502bba4777ebfa6fae3d3bc418297acab4f26b04b42e3c31b36c735399",
|
||||
"internal/handler/routesGen.go": "76c1a5b2644e75e56553dc6e9e862a63128d902dd94807ce7c8aee5491cd1e3e",
|
||||
"internal/middleware/registryGen.go": "e5f47efa57a1432765ef3419bae59dbf3b4d55b3aac0a6c935ce7a52ba6b47f4",
|
||||
"internal/types/typesGen.go": "7586e86fa5d67051283178cc578c890f6c02fd2d5c383170a71a39c2e77ce344",
|
||||
"main.go": "09ae81857b806f3f7dca608399b1881d94bc9c4b2258e774e77d694fcadbab65"
|
||||
"main.go": "a12db5b7df0f2fdca4939a7382daee04c85ddb63ef0bccd1a2ce26b718f42579"
|
||||
},
|
||||
"contract": {
|
||||
"routes": {
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
This checked-in example uses a relative local framework replacement (`../..`).
|
||||
Run `go mod tidy && go run .` here. No external database or cloud account is required.
|
||||
The Go-only error catalog is `internal/apierrors/catalog.go`, registered by svc.
|
||||
The generated main and handlers are unmodified. Try:
|
||||
The generated main and handlers are unmodified. Built-in endpoint switches and
|
||||
paths are configured in etc/config.yaml under server.endpoints. Health replies
|
||||
use the project envelope by default; responseMode: raw opts out. Readiness
|
||||
failures stay HTTP 503, independently of business error status policies.
|
||||
|
||||
Dependencies can call svc.RequestShutdown(err); the generated entry drains
|
||||
requests, closes resources and retains the failure for a nonzero exit.
|
||||
Try:
|
||||
|
||||
```sh
|
||||
curl -i 'http://127.0.0.1:8080/check?kind=fixed'
|
||||
|
||||
@@ -4,6 +4,10 @@ server:
|
||||
enableLogger: true
|
||||
slowRequestThreshold: 500ms
|
||||
shutdownTimeout: 10s
|
||||
endpoints:
|
||||
liveness: {enabled: true, path: /health/live, responseMode: app}
|
||||
readiness: {enabled: true, path: /health/ready, responseMode: app}
|
||||
openapi: {enabled: true, path: /openapi.json}
|
||||
log:
|
||||
service: "example.com/managed"
|
||||
level: info
|
||||
|
||||
@@ -2,7 +2,7 @@ module example.com/managed
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.12.0-preview
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.13.0-preview
|
||||
|
||||
require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -41,12 +41,17 @@ func run(ctx context.Context, path string) (runErr error) {
|
||||
}
|
||||
// RunContext drains HTTP requests before returning. This scope owns dependencies
|
||||
// and closes them exactly once, before closing the borrowed logger.
|
||||
defer func() { runErr = errors.Join(runErr, deps.Close()) }()
|
||||
defer func() {
|
||||
runErr = errors.Join(runErr, deps.Close())
|
||||
if cause := deps.Lifecycle.Err(); cause != nil && !errors.Is(runErr, cause) {
|
||||
runErr = errors.Join(runErr, cause)
|
||||
}
|
||||
}()
|
||||
app, err := handler.NewApp(deps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { runErr = errors.Join(runErr, app.Close()) }()
|
||||
logger.InfoContext(ctx, "service.starting", "address", app.Config().Address)
|
||||
return app.RunContext(ctx)
|
||||
return app.RunContext(deps.Lifecycle.Context())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user