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

@@ -3,14 +3,15 @@ package health
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/response"
)
const (
@@ -38,6 +39,7 @@ type CheckResult struct {
type Result struct {
Status string `json:"status"`
Checks []CheckResult `json:"checks,omitempty"`
cause error
}
func (r Result) Ready() bool { return r.Status == StatusReady }
@@ -118,6 +120,7 @@ func (c *Checker) Check(ctx context.Context) Result {
result.Status = StatusNotReady
checkResult.Status = StatusNotReady
checkResult.Error = err.Error()
result.cause = errors.Join(result.cause, fmt.Errorf("%s: %w", probe.Name, err))
}
result.Checks = append(result.Checks, checkResult)
}
@@ -126,28 +129,78 @@ func (c *Checker) Check(ctx context.Context) Result {
// LivenessHandler reports that the process is running. It does not execute
// dependency probes and is safe to use for orchestrator liveness checks.
func (c *Checker) LivenessHandler() httpx.Handler {
func (c *Checker) LivenessHandler(options ...HandlerOption) httpx.Handler {
settings := handlerSettings(options)
return func(ctx *httpx.Context) error {
return ctx.OK(map[string]string{"status": "ok"})
data := map[string]string{"status": "ok"}
if settings.raw {
return ctx.JSON(http.StatusOK, data)
}
return ctx.RespondStatus(http.StatusOK, data, nil)
}
}
// ReadinessHandler reports whether all registered dependencies are ready.
// Failure uses HTTP 503 while preserving GoOmni's response envelope.
func (c *Checker) ReadinessHandler() httpx.Handler {
// Failure always uses HTTP 503. App response configuration is used by default;
// WithRawResponse returns just health data. Internal probe errors remain in the
// recorded failure for logs, not in the HTTP response.
func (c *Checker) ReadinessHandler(options ...HandlerOption) httpx.Handler {
settings := handlerSettings(options)
return func(ctx *httpx.Context) error {
result := c.Check(ctx.Context())
var result Result
if settings.shutdown != nil && settings.shutdown.Err() != nil {
result = Result{Status: StatusNotReady, cause: context.Cause(settings.shutdown)}
} else {
result = c.Check(ctx.Context())
// A stop may arrive while the probes are running.
if settings.shutdown != nil && settings.shutdown.Err() != nil {
result.Status = StatusNotReady
result.cause = errors.Join(result.cause, context.Cause(settings.shutdown))
}
}
for i := range result.Checks {
result.Checks[i].Error = ""
}
status := http.StatusOK
code, message := "READY", "ready"
var failure error
if !result.Ready() {
status = http.StatusServiceUnavailable
code, message = "NOT_READY", "service is not ready"
failure = apierrors.New(status, "NOT_READY", "service is not ready").WithCause(result.cause)
}
return ctx.JSON(status, response.Response{
Code: code,
Message: message,
Data: result,
RequestID: ctx.RequestID(),
})
if settings.raw {
if failure != nil {
resolved := ctx.ResolveError(failure)
resolved.Status = status
ctx.RecordFailure(resolved)
}
return ctx.JSON(status, result)
}
return ctx.RespondStatus(status, result, failure)
}
}
type handlerConfig struct {
raw bool
shutdown context.Context
}
type HandlerOption func(*handlerConfig)
// WithRawResponse bypasses the App envelope for this probe's health data.
func WithRawResponse() HandlerOption { return func(c *handlerConfig) { c.raw = true } }
// WithShutdownContext marks readiness false as soon as service shutdown begins.
// It never changes liveness or closes any project resource.
func WithShutdownContext(ctx context.Context) HandlerOption {
return func(c *handlerConfig) { c.shutdown = ctx }
}
func handlerSettings(options []HandlerOption) handlerConfig {
var cfg handlerConfig
for _, option := range options {
if option != nil {
option(&cfg)
}
}
return cfg
}