Repository navigation
Replies: 3 comments
Revision: health is a Service, not part of the lifecycleThis revision applies the decisions in the lifecycle proposal's revision. The lifecycle no longer owns readiness: This answers open questions 1 and 6. Each section below replaces the section of the same name in the post. Out of scope and References are unchanged and still apply. Summary
BackgroundUnchanged, except the Kubernetes bullet, which now reads:
Proposed APIpackage health
type Monitor interface {
Healthy(ctx context.Context) (bool, error)
}
// MonitorFunc adapts a function to Monitor. New.
type MonitorFunc func(ctx context.Context) (bool, error)
func (f MonitorFunc) Healthy(ctx context.Context) (bool, error)
// Unchanged: Binary, And, Or, AndMonitor, OrMonitor.
// Probes is an App's health as an orchestrator sees it.
type Probes struct {
Liveness Monitor
Readiness Monitor
}
// HTTPHandler serves GET /health/liveness and GET /health/readiness. It
// answers 200 when the probe is healthy and 503 when it is not, and logs a
// monitor's error rather than returning it to the caller.
func HTTPHandler(p Probes, log *slog.Logger) http.Handler
// Service is a humus.Service that serves an App's probes with HTTPHandler,
// on a listener of its own.
type Service struct{ /* unexported */ }
type Option func(*options)
// Liveness adds m to the Service's liveness probe.
func Liveness(m Monitor) Option
// Readiness adds m to the Service's readiness probe.
func Readiness(m Monitor) Option
// NewService listens on settings.Addr through s.Host, and logs through
// s.Telemetry.
func NewService(s *humus.Setup, settings Settings, opts ...Option) (*Service, error)
// Probes returns the Service's probes, its own readiness included, for
// another server to expose, such as gRPC health v1.
func (s *Service) Probes() Probes
func (s *Service) Serve(ctx context.Context) error
func (s *Service) Shutdown(ctx context.Context) error
// Settings has one setting, HUMUS_HEALTH_ADDR, which defaults to ":8081".
type Settings struct {
Addr string
}
func ReadSettings(ctx context.Context, env bedrock.Environment) (Settings, error)
In use: func Init(ctx context.Context, s *humus.Setup) ([]humus.Service, error) {
// ... pool, api as in the lifecycle proposal ...
hs, err := health.ReadSettings(ctx, s.Process.Environment)
if err != nil {
return nil, err
}
probes, err := health.NewService(s, hs, health.Readiness(health.MonitorFunc(func(ctx context.Context) (bool, error) {
return pool.Ping(ctx) == nil, nil
})))
if err != nil {
return nil, err
}
return []humus.Service{api, probes}, nil
}A runtime package's App, such as REST's, does this for the user by default. Each wave-2 proposal says how its users add monitors, and how they turn the health Service off. gRPC health v1Proposed here, and implemented in Proposal: gRPC services:
grpc-go's health server already has Design decisions
Testingfunc TestReadinessFailsDuringTheDrainDelay(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
host := humustest.NewHost(t)
app := humus.App{
Host: host.Host(),
Telemetry: telemetry.NoopBuilder,
DrainDelay: 5 * time.Second,
Init: func(ctx context.Context, s *humus.Setup) ([]humus.Service, error) {
svc, err := health.NewService(s, health.Settings{Addr: ":8081"})
if err != nil {
return nil, err
}
return []humus.Service{svc}, nil
},
}
p := humustest.Start(t, app)
synctest.Wait()
// probe GETs a path on :8081 through host.HTTPClient() and returns the status code.
require.Equal(t, http.StatusOK, probe(t, host, "/health/readiness"))
p.Signal(syscall.SIGTERM)
synctest.Wait() // shutdown has started; the drain delay is running
require.Equal(t, http.StatusServiceUnavailable, probe(t, host, "/health/readiness"))
require.Equal(t, http.StatusOK, probe(t, host, "/health/liveness"))
require.Zero(t, p.Wait().Code())
})
}The Migration from today
Open questionsQuestion 1 is answered: explicit at the root, a default in the runtime packages. Question 6 is answered by the lifecycle: the root has no drain-delay default, and each runtime package's App picks its own. The rest are still open:
|
Revision 2: health's core, with exposing it split outThis revision narrows this proposal to health's core: the Exposing the probes is split out. That covers HTTP, either on a server of their own or attached to one an App already runs, and gRPC health v1. It needs the HTTP server foundation and gRPC services proposals, so it moves to a proposal of its own in wave 3, Proposal: exposing health probes, as the charter's revision 3 plans. It also applies where Proposal: configuration for humus apps landed. The core opens nothing and logs nothing, so health has no Each section below replaces the section of the same name, as amended by the first revision. Background and References are unchanged and still apply. Summary
Proposed APIpackage health
type Monitor interface {
Healthy(ctx context.Context) (bool, error)
}
// MonitorFunc adapts a function to Monitor. New.
type MonitorFunc func(ctx context.Context) (bool, error)
func (f MonitorFunc) Healthy(ctx context.Context) (bool, error)
// Unchanged: Binary, And, Or, AndMonitor, OrMonitor.
// Probes is an App's health as an orchestrator sees it.
type Probes struct {
Liveness Monitor
Readiness Monitor
}
// Service is a humus.Service that owns an App's readiness. It opens no
// listener and serves no protocol.
type Service struct{ /* unexported */ }
type Option func(*options)
// Liveness adds m to the Service's liveness probe.
func Liveness(m Monitor) Option
// Readiness adds m to the Service's readiness probe.
func Readiness(m Monitor) Option
func NewService(opts ...Option) *Service
// Probes returns the Service's probes, its own readiness included.
func (s *Service) Probes() Probes
// Serve marks the Service ready, and returns nil once ctx is done or
// Shutdown is called, marking it not ready for good.
func (s *Service) Serve(ctx context.Context) error
func (s *Service) Shutdown(ctx context.Context) error
In use, in the end user's func Init(ctx context.Context, s *humus.Setup) ([]humus.Service, error) {
// ... pool, api as in the lifecycle proposal ...
hs := health.NewService(health.Readiness(health.MonitorFunc(func(ctx context.Context) (bool, error) {
return pool.Ping(ctx) == nil, nil
})))
// hs.Probes() is what Proposal: exposing health probes serves.
return []humus.Service{api, hs}, nil
}Design decisionsThe decisions of the first revision stand, except for these.
Testingfunc TestServiceIsNotReadyDuringTheDrainDelay(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hs := health.NewService()
readiness := hs.Probes().Readiness
app := humus.App{
Host: humustest.NewHost(t).Host(),
Telemetry: telemetry.NoopBuilder,
ShutdownGracePeriod: 10 * time.Second,
DrainDelay: 5 * time.Second,
Init: func(context.Context, *humus.Setup) ([]humus.Service, error) {
return []humus.Service{hs}, nil
},
}
p := bedrocktest.NewProcess(t)
status := make(chan bedrock.ExitStatus, 1)
go func() { status <- app.Run(t.Context(), p.Process()) }()
<-p.Notified(syscall.SIGTERM)
synctest.Wait() // Serve has started
ready, err := readiness.Healthy(t.Context())
require.NoError(t, err)
require.True(t, ready)
p.Signal(syscall.SIGTERM)
synctest.Wait() // shutdown has started; the drain delay is running
ready, err = readiness.Healthy(t.Context())
require.NoError(t, err)
require.False(t, ready)
require.Zero(t, (<-status).Code())
})
}
Migration from today
Open questionsThe first revision's questions 2 (paths) and 3 (response bodies) are about exposing probes, so they move to Proposal: exposing health probes. Question 3 may also need names on monitors, and that proposal adds them if it chooses to. These are still open here:
Consequences for the other proposals
Out of scope
|
Closing: health's core has landedRevision 2's stories are done:
Revision 2's open questions 4 (startup probes) and 5 (caching) move to Proposal: exposing health probes, in wave 3 of the charter, with questions 2 and 3. Both depend on how the probes are served and how often they're checked. If that proposal gives startup probes a probe of their own, its stories add the field to |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Note
The Summary, Proposed API, design decisions, testing, migration, open questions and out of scope are amended again by revision 2. This proposal is narrowed to health's core:
Monitor,Probes, and a health Service that owns readiness and serves no protocol. Exposing the probes over HTTP and gRPC moves to Proposal: exposing health probes, in wave 3, as the charter's revision 3 plans. health has no settings.Note
The Summary, Proposed API, design decisions, testing, migration and open questions in this post are superseded by the revision below. It applies the decisions of the lifecycle proposal. Health is a Service that owns readiness, instead of being part of the lifecycle, and open questions 1 and 6 are answered. The rest of this post is unchanged and kept for context.
Note
Part of the humus re-evaluation; see Proposal: humus on the bedrock process model. It depends on Proposal: the humus App lifecycle, which owns readiness. Proposal: HTTP server foundation and Proposal: gRPC services use it.
Summary
health.Monitorand its combinators.Init.Background
The
healthpackage is pure (health/health.go):MonitorisHealthy(ctx) (bool, error);Binaryis an atomic flag whose zero value is unhealthy;Andstops at the first failure, andOrjoins the errors.It has no globals and no I/O, and it probably survives as it is.
HTTP. The README promises
/health/livenessand/health/readiness, butresthas served no health endpoints since story(issue-484): rest migrate to bedrock v0210 #485.gRPC.
grpc.Api.RegisterServiceregisters a monitor under a service's name when the implementation happens to be ahealth.Monitor(grpc/api.go#L66-L67).internal/grpchealthserves it, with these gaps:"", which Kubernetes' gRPC probe andgrpc-health-probecheck by default, so the overall check answersNotFound(internal/grpchealth/server.go#L56).SERVICE_UNKNOWN(L70), which the protocol reserves for aWatchon an unknown service.Watch. It polls every 10s (L36).humus.Logger, the global (L34).Kubernetes. Readiness should fail as soon as shutdown starts, and load balancers need time to stop routing to the pod. That is the lifecycle's
HUMUS_SHUTDOWN_DRAIN_DELAY.Consumers and jobs. Queue consumers serve no traffic but still need liveness in Kubernetes. Jobs need neither probe.
Proposed API
The root package adds three methods to
Setup, as proposed in Proposal: the humus App lifecycle:And(lifecycle, user monitors…). The lifecycle's part is:Init;Servehas started;And(user monitors…), and is healthy when there are none.In use:
gRPC health v1
Proposed here, and implemented in Proposal: gRPC services:
""Andwith the App's readinessNotFoundforCheck,SERVICE_UNKNOWNforWatchNOT_SERVING, and the error is loggedDesign decisions
Initlike any other, so jobs don't open a port they don't need.Monitorstays one method. Failures are logged; they are not exposed in the response.timeoutSecondsalready bounds the request.Testing
The
Monitorcombinators keep their table tests, andhealth.Servicepasseshumustest.TestService.Migration from today
health.Monitor,Binary,AndandOrare unchanged.grpc.Api.RegisterServicedetecting ahealth.Monitorbecomes explicit registration (Proposal: gRPC services).internal/grpchealthis replaced, and gets an entry for"".health.Serviceon its own port.Open questions
Init, or one thathumus.Appalways runs whenHUMUS_HEALTH_ADDRis set?/health/livenessand/health/readiness, or use Kubernetes'/livezand/readyz?Init, or a probe of its own?Out of scope
References
health/health.gointernal/grpchealth/server.goAll reactions