hosting
packageAPI reference for the hosting
package.
Imports
(16)github.com/mirkobrombin/go-foundation/v2/core/contracts
STD
context
STD
errors
STD
fmt
STD
net/http
STD
os
STD
os/signal
STD
sync
STD
sync/atomic
STD
syscall
STD
time
INT
github.com/mirkobrombin/go-foundation/v2/app/di
INT
github.com/mirkobrombin/go-foundation/v2/app/web
INT
github.com/mirkobrombin/go-foundation/v2/core/health
STD
strings
STD
testing
HostState
HostState represents the current lifecycle state of a Host.
type HostState int32
Host
Host manages the lifecycle of the application.
type Host struct
Methods
Parameters
Returns
func (*Host) Run(ctx context.Context) error
{
if !h.runStarted.CompareAndSwap(false, true) {
return ErrHostAlreadyRun
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
h.mu.Lock()
h.cancel = cancel
h.mu.Unlock()
h.state.Store(int32(HostStarting))
for _, fn := range h.onStart {
if err := safeLifecycleCall("on-start callback", func() error {
fn()
return nil
}); err != nil {
cancel()
h.state.Store(int32(HostStopping))
cleanupDone := make(chan error, 1)
go func() {
cleanupDone <- h.finishShutdown()
}()
select {
case cleanupErr := <-cleanupDone:
return errors.Join(err, cleanupErr)
case <-time.After(h.shutdownTimeout()):
return errors.Join(
err,
fmt.Errorf("host shutdown timed out after %s", h.shutdownTimeout()),
)
}
}
}
startupCtx, startupCancel := context.WithTimeout(ctx, h.startupTimeout)
defer startupCancel()
startedHosted := 0
for _, svc := range h.hostedServices {
startResult := make(chan error, 1)
go func(service HostedService) {
startResult <- safeLifecycleCall("hosted service start", func() error {
return service.Start(ctx)
})
}(svc)
select {
case startErr := <-startResult:
if startErr != nil {
startupCancel()
cancel()
h.state.Store(int32(HostStopping))
cleanupDone := make(chan error, 1)
go func() {
stopErr := <-h.beginHostedShutdown(startedHosted)
cleanupDone <- errors.Join(stopErr, h.finishShutdown())
}()
select {
case cleanupErr := <-cleanupDone:
return errors.Join(fmt.Errorf("hosted service start failed: %w", startErr), cleanupErr)
case <-time.After(h.shutdownTimeout()):
return errors.Join(
fmt.Errorf("hosted service start failed: %w", startErr),
fmt.Errorf("host shutdown timed out after %s", h.shutdownTimeout()),
)
}
}
startedHosted++
case <-startupCtx.Done():
startErr := startupCtx.Err()
startupCancel()
cancel()
h.state.Store(int32(HostStopping))
cleanupDone := make(chan error, 1)
go func() {
stopErr := <-h.beginHostedShutdown(startedHosted)
lateErr := <-startResult
if lateErr == nil {
stopErr = errors.Join(stopErr, h.stopHostedService(svc))
}
cleanupDone <- errors.Join(lateErr, stopErr, h.finishShutdown())
}()
select {
case cleanupErr := <-cleanupDone:
return errors.Join(fmt.Errorf("hosted service start failed: %w", startErr), cleanupErr)
case <-time.After(h.shutdownTimeout()):
return errors.Join(
fmt.Errorf("hosted service start failed: %w", startErr),
fmt.Errorf("host startup cleanup timed out after %s", h.shutdownTimeout()),
)
}
}
}
var wg sync.WaitGroup
errCh := make(chan error, len(h.services)+len(h.hostedServices))
for i := 0; i < startedHosted; i++ {
service, ok := h.hostedServices[i].(interface{ Completion() <-chan error })
if !ok {
continue
}
completion := service.Completion()
if completion == nil {
continue
}
go func() {
select {
case err := <-completion:
if err == nil {
err = errors.New("hosted background service stopped unexpectedly")
}
select {
case errCh <- err:
case <-ctx.Done():
}
case <-ctx.Done():
}
}()
}
for _, svc := range h.services {
s := svc
wg.Add(1)
go func() {
defer wg.Done()
err := safeLifecycleCall("background service execute", func() error {
return s.Execute(ctx)
})
if err == nil {
if ctx.Err() != nil {
return
}
err = errors.New("background service stopped unexpectedly")
}
select {
case errCh <- err:
case <-ctx.Done():
}
}()
}
h.state.Store(int32(HostRunning))
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigCh)
var firstErr error
select {
case <-sigCh:
case <-ctx.Done():
case err := <-errCh:
firstErr = err
}
cancel()
h.state.Store(int32(HostStopping))
cleanupDone := make(chan error, 1)
hostedDone := h.beginHostedShutdown(startedHosted)
go func() {
stopErr := <-hostedDone
wg.Wait()
cleanupDone <- errors.Join(stopErr, h.finishShutdown())
}()
timeout := h.shutdownTimeout()
select {
case stopErr := <-cleanupDone:
return errors.Join(firstErr, stopErr)
case <-time.After(timeout):
return errors.Join(
firstErr,
fmt.Errorf("host shutdown timed out after %s", timeout),
)
}
}
Parameters
Returns
func (*Host) stopHostedService(service HostedService) error
{
stopCtx, cancel := context.WithTimeout(context.Background(), h.shutdownTimeout())
defer cancel()
if err := safeLifecycleCall("hosted service stop", func() error {
return service.Stop(stopCtx)
}); err != nil {
return fmt.Errorf("hosted service stop failed: %w", err)
}
return nil
}
Parameters
Returns
func (*Host) beginHostedShutdown(started int) <-chan error
{
done := make(chan error, 1)
go func() {
var errs []error
for i := started - 1; i >= 0; i-- {
if err := h.stopHostedService(h.hostedServices[i]); err != nil {
errs = append(errs, err)
}
}
done <- errors.Join(errs...)
}()
return done
}
Returns
func (*Host) shutdownTimeout() time.Duration
{
if h.ShutdownTimeout > 0 {
return h.ShutdownTimeout
}
return 30 * time.Second
}
Returns
func (*Host) finishShutdown() error
{
var errs []error
for _, fn := range h.onStop {
if err := safeLifecycleCall("on-stop callback", func() error {
fn()
return nil
}); err != nil {
errs = append(errs, err)
}
}
if err := safeLifecycleCall("container close", h.closeContainer); err != nil {
errs = append(errs, err)
}
h.state.Store(int32(HostStopped))
return errors.Join(errs...)
}
Returns
func (*Host) closeContainer() error
{
if h.Container == nil {
return nil
}
return h.Container.Close()
}
Parameters
Returns
func (*Host) Shutdown(_ context.Context) error
{
h.mu.Lock()
cancel := h.cancel
h.mu.Unlock()
if cancel != nil {
cancel()
}
return nil
}
Parameters
func (*Host) OnStart(fn func())
{
h.onStart = append(h.onStart, fn)
}
Parameters
func (*Host) OnStop(fn func())
{
h.onStop = append(h.onStop, fn)
}
Parameters
func (*Host) AddHostedService(svc HostedService)
{
h.hostedServices = append(h.hostedServices, svc)
}
Fields
| Name | Type | Description |
|---|---|---|
| services | []BackgroundService | |
| hostedServices | []HostedService | |
| onStart | []func() | |
| onStop | []func() | |
| Container | *di.Container | |
| Server | *web.Server | |
| HealthRegistry | *health.Registry | |
| cancel | context.CancelFunc | |
| mu | sync.RWMutex | |
| runStarted | atomic.Bool | |
| state | atomic.Int32 | |
| ShutdownTimeout | time.Duration | |
| startupTimeout | time.Duration |
BackgroundService
BackgroundService is a long-running service started in parallel.
type BackgroundService interface
Methods
HostedService
HostedService is a managed lifecycle service with explicit Start/Stop.
type HostedService interface
Methods
BackgroundServiceAdapter
BackgroundServiceAdapter wraps a BackgroundService as a HostedService.
type BackgroundServiceAdapter struct
Methods
Parameters
Returns
func (*BackgroundServiceAdapter) Start(ctx context.Context) error
{
if a.Svc == nil {
return fmt.Errorf("hosting: background service is nil")
}
a.mu.Lock()
defer a.mu.Unlock()
if a.cancel != nil {
return fmt.Errorf("hosting: background service already started")
}
runCtx, cancel := context.WithCancel(ctx)
done := make(chan struct{})
completion := make(chan error, 1)
a.cancel = cancel
a.done = done
a.completion = completion
go func() {
err := safeLifecycleCall("background service execute", func() error {
return a.Svc.Execute(runCtx)
})
a.mu.Lock()
a.err = err
a.mu.Unlock()
completion <- err
close(completion)
close(done)
}()
return nil
}
Completion reports when the adapted background service exits.
Returns
func (*BackgroundServiceAdapter) Completion() <-chan error
{
a.mu.Lock()
defer a.mu.Unlock()
return a.completion
}
Parameters
Returns
func (*BackgroundServiceAdapter) Stop(ctx context.Context) error
{
a.mu.Lock()
cancel := a.cancel
done := a.done
a.mu.Unlock()
if cancel == nil {
return nil
}
cancel()
select {
case <-done:
a.mu.Lock()
err := a.err
a.mu.Unlock()
return err
case <-ctx.Done():
return ctx.Err()
}
}
Fields
| Name | Type | Description |
|---|---|---|
| Svc | BackgroundService | |
| mu | sync.Mutex | |
| cancel | context.CancelFunc | |
| done | chan struct{} | |
| completion | chan error | |
| err | error |
safeLifecycleCall
Parameters
Returns
func safeLifecycleCall(name string, call func() error) (err error)
{
if call == nil {
return fmt.Errorf("hosting: %s is nil", name)
}
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("hosting: %s panic: %v", name, recovered)
}
}()
return call()
}
HostBuilder
HostBuilder provides a fluent API for constructing a Host.
type HostBuilder struct
Methods
Parameters
Returns
func (*HostBuilder) ConfigureServices(fn func(*di.Builder)) *HostBuilder
{
if b.di == nil {
b.di = di.NewBuilder()
}
fn(b.di)
return b
}
Parameters
Returns
func (*HostBuilder) ConfigureWeb(fn func(*web.Server)) *HostBuilder
{
if b.web == nil {
b.web = web.New()
}
fn(b.web)
return b
}
UseWeb attaches an existing web server to the host.
Parameters
Returns
func (*HostBuilder) UseWeb(server *web.Server) *HostBuilder
{
b.web = server
return b
}
UseContainer attaches an existing dependency container to the host.
Parameters
Returns
func (*HostBuilder) UseContainer(container *di.Container) *HostBuilder
{
b.container = container
return b
}
Parameters
Returns
func (*HostBuilder) AddService(svc BackgroundService) *HostBuilder
{
b.services = append(b.services, svc)
return b
}
Parameters
Returns
func (*HostBuilder) AddHostedService(svc HostedService) *HostBuilder
{
b.hostedServices = append(b.hostedServices, svc)
return b
}
Parameters
Returns
func (*HostBuilder) OnStart(fn func()) *HostBuilder
{
b.onStart = append(b.onStart, fn)
return b
}
Parameters
Returns
func (*HostBuilder) OnStop(fn func()) *HostBuilder
{
b.onStop = append(b.onStop, fn)
return b
}
Parameters
Returns
func (*HostBuilder) WithAddr(addr string) *HostBuilder
{
b.webAddr = addr
return b
}
WithTLS configures the web host to serve HTTPS with the given certificate and key.
Parameters
Returns
func (*HostBuilder) WithTLS(certFile, keyFile string) *HostBuilder
{
if certFile == "" || keyFile == "" {
panic("hosting: TLS certificate and key files are required")
}
b.tlsCertFile = certFile
b.tlsKeyFile = keyFile
return b
}
Parameters
Returns
func (*HostBuilder) WithShutdownTimeout(d time.Duration) *HostBuilder
{
b.shutdownTimeout = d
return b
}
Parameters
Returns
func (*HostBuilder) WithStartupTimeout(d time.Duration) *HostBuilder
{
b.startupTimeout = d
return b
}
Parameters
Returns
func (*HostBuilder) WithHealthRegistry(r *health.Registry) *HostBuilder
{
b.healthRegistry = r
return b
}
Returns
func (*HostBuilder) Build() *Host
{
h := &Host{
services: append([]BackgroundService{}, b.services...),
hostedServices: append([]HostedService{}, b.hostedServices...),
onStart: b.onStart,
onStop: b.onStop,
ShutdownTimeout: b.shutdownTimeout,
startupTimeout: b.startupTimeout,
HealthRegistry: b.healthRegistry,
Container: b.container,
}
if b.web != nil {
addr := b.webAddr
if addr == "" {
addr = "127.0.0.1:8080"
}
s := &webService{
server: b.web,
addr: addr,
certFile: b.tlsCertFile,
keyFile: b.tlsKeyFile,
}
h.services = append(h.services, s)
h.Server = b.web
}
if h.HealthRegistry != nil && h.Server != nil {
if err := h.Server.RegisterRoutes(
web.RouteDefinition{
Method: http.MethodGet,
Path: "/health/live",
Handler: func(ctx *web.Context) error {
return ctx.JSON(200, map[string]string{"status": "alive"})
},
},
web.RouteDefinition{
Method: http.MethodGet,
Path: "/health/ready",
Handler: func(ctx *web.Context) error {
results := h.HealthRegistry.CheckAll(ctx.Request.Context())
healthy := true
details := make(map[string]string, len(results))
for name, report := range results {
details[name] = report.Status.String()
if report.Status == health.StatusUnhealthy {
healthy = false
}
}
code := 200
status := "ready"
if !healthy {
code = 503
status = "not ready"
}
return ctx.JSON(code, map[string]any{
"status": status,
"details": details,
})
},
},
); err != nil {
panic(fmt.Errorf("hosting: register health endpoints: %w", err))
}
}
if b.di != nil {
if h.Container != nil {
panic("hosting: container already configured")
}
c, err := b.di.Build()
if err != nil {
panic(err)
}
h.Container = c
}
return h
}
Fields
| Name | Type | Description |
|---|---|---|
| services | []BackgroundService | |
| hostedServices | []HostedService | |
| onStart | []func() | |
| onStop | []func() | |
| di | *di.Builder | |
| web | *web.Server | |
| webAddr | string | |
| tlsCertFile | string | |
| tlsKeyFile | string | |
| shutdownTimeout | time.Duration | |
| startupTimeout | time.Duration | |
| healthRegistry | *health.Registry | |
| container | *di.Container |
NewBuilder
NewBuilder creates a new HostBuilder.
Returns
func NewBuilder() *HostBuilder
{
return &HostBuilder{
startupTimeout: 15 * time.Second,
}
}
webService
type webService struct
Methods
Parameters
Returns
func (*webService) Execute(ctx context.Context) error
{
errCh := make(chan error, 1)
go func() {
if w.certFile != "" {
errCh <- w.server.ListenAndServeTLS(w.addr, w.certFile, w.keyFile)
return
}
errCh <- w.server.ListenAndServe(w.addr)
}()
select {
case err := <-errCh:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-w.server.Started():
}
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return w.server.Shutdown(shutdownCtx)
case err := <-errCh:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
}
Fields
| Name | Type | Description |
|---|---|---|
| server | *web.Server | |
| addr | string | |
| certFile | string | |
| keyFile | string |
fakeSvc
type fakeSvc struct
Methods
Parameters
Returns
func (*fakeSvc) Execute(ctx context.Context) error
{
f.running.Store(true)
<-ctx.Done()
f.running.Store(false)
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| running | atomic.Bool |
TestBuilder_ConfigureServices
Parameters
func TestBuilder_ConfigureServices(t *testing.T)
{
h := NewBuilder().
ConfigureServices(func(b *di.Builder) {
di.RegisterInstance[*fakeSvc](b, &fakeSvc{})
}).
Build()
if h.Container == nil {
t.Fatal("Container should not be nil")
}
svc := di.ResolveType[*fakeSvc](h.Container)
if svc == nil {
t.Fatal("should resolve fakeSvc")
}
}
TestBuilder_ConfigureWeb
Parameters
func TestBuilder_ConfigureWeb(t *testing.T)
{
h := NewBuilder().
ConfigureWeb(func(app *web.Server) {
app.MapGet("/test", func(c *web.Context) error {
c.String(200, "ok")
return nil
})
}).
WithAddr(":0").
Build()
if h.Server == nil {
t.Fatal("Server should not be nil")
}
if len(h.services) != 1 {
t.Fatalf("expected 1 service (web), got %d", len(h.services))
}
}
TestBuilder_UseWeb
Parameters
func TestBuilder_UseWeb(t *testing.T)
{
server := web.New()
host := NewBuilder().UseWeb(server).Build()
if host.Server != server {
t.Fatal("UseWeb() did not attach the server")
}
if len(host.services) != 1 {
t.Fatalf("host services = %d, want 1", len(host.services))
}
}
TestWebServiceHandlesImmediateCancellation
Parameters
func TestWebServiceHandlesImmediateCancellation(t *testing.T)
{
service := &webService{server: web.New(), addr: ":0"}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := service.Execute(ctx); err != nil {
t.Fatalf("Execute() error = %v", err)
}
}
TestBuilder_AddService
Parameters
func TestBuilder_AddService(t *testing.T)
{
svc := &fakeSvc{}
host := NewBuilder().AddService(svc).Build()
if len(host.services) != 1 {
t.Fatalf("expected 1 service, got %d", len(host.services))
}
}
TestBuilder_WithAddr
Parameters
func TestBuilder_WithAddr(t *testing.T)
{
b := NewBuilder().WithAddr(":1234")
if b.webAddr != ":1234" {
t.Errorf("addr: got %q, want :1234", b.webAddr)
}
}
TestHost_Lifecycle
Parameters
func TestHost_Lifecycle(t *testing.T)
{
svc := &fakeSvc{}
host := NewBuilder().AddService(svc).Build()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
host.Run(ctx)
close(done)
}()
time.Sleep(20 * time.Millisecond)
if !svc.running.Load() {
t.Error("service should be running")
}
cancel()
<-done
time.Sleep(20 * time.Millisecond)
if svc.running.Load() {
t.Error("service should have shut down")
}
}
TestHost_RunIsOneShot
Parameters
func TestHost_RunIsOneShot(t *testing.T)
{
svc := &fakeSvc{}
host := NewBuilder().AddService(svc).Build()
ctx, cancel := context.WithCancel(context.Background())
firstDone := make(chan error, 1)
go func() {
firstDone <- host.Run(ctx)
}()
deadline := time.Now().Add(time.Second)
for host.State() != HostRunning && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if host.State() != HostRunning {
t.Fatal("first Run() did not start the host")
}
if err := host.Run(context.Background()); !errors.Is(err, ErrHostAlreadyRun) {
t.Fatalf("concurrent Run() error = %v, want %v", err, ErrHostAlreadyRun)
}
cancel()
if err := <-firstDone; err != nil {
t.Fatalf("first Run() error = %v", err)
}
if err := host.Run(context.Background()); !errors.Is(err, ErrHostAlreadyRun) {
t.Fatalf("second Run() error = %v, want %v", err, ErrHostAlreadyRun)
}
}
panicBackgroundService
type panicBackgroundService struct
Methods
Parameters
Returns
func (*panicBackgroundService) Execute(context.Context) error
{
panic("background failed")
}
TestHostRecoversBackgroundServicePanic
Parameters
func TestHostRecoversBackgroundServicePanic(t *testing.T)
{
host := NewBuilder().AddService(&panicBackgroundService{}).Build()
err := host.Run(context.Background())
if err == nil || !strings.Contains(err.Error(), "background failed") {
t.Fatalf("Run() error = %v", err)
}
if host.State() != HostStopped {
t.Fatalf("state = %s, want stopped", host.State())
}
}
TestHostOnStartPanicStillClosesContainer
Parameters
func TestHostOnStartPanicStillClosesContainer(t *testing.T)
{
closer := &trackedCloser{}
builder := di.NewBuilder()
di.RegisterInstance(builder, closer)
host := NewBuilder().
UseContainer(builder.MustBuild()).
OnStart(func() { panic("start failed") }).
Build()
err := host.Run(context.Background())
if err == nil || !strings.Contains(err.Error(), "start failed") {
t.Fatalf("Run() error = %v", err)
}
if !closer.closed.Load() {
t.Fatal("OnStart panic skipped container cleanup")
}
}
fakeHosted
type fakeHosted struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| started | atomic.Bool | |
| stopped | atomic.Bool | |
| startErr | error |
TestHostedService_Lifecycle
Parameters
func TestHostedService_Lifecycle(t *testing.T)
{
svc := &fakeHosted{}
host := NewBuilder().AddHostedService(svc).Build()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
host.Run(ctx)
close(done)
}()
time.Sleep(50 * time.Millisecond)
if !svc.started.Load() {
t.Error("hosted service should be started")
}
cancel()
<-done
if !svc.stopped.Load() {
t.Error("hosted service should be stopped")
}
}
TestHostedService_StartupFailure
Parameters
func TestHostedService_StartupFailure(t *testing.T)
{
failSvc := &fakeHosted{startErr: context.DeadlineExceeded}
host := NewBuilder().AddHostedService(failSvc).Build()
err := host.Run(context.Background())
if err == nil {
t.Fatal("expected error from startup failure")
}
}
TestHost_StartupTimeout
Parameters
func TestHost_StartupTimeout(t *testing.T)
{
slowSvc := &slowHosted{}
host := NewBuilder().
AddHostedService(slowSvc).
WithStartupTimeout(100 * time.Millisecond).
Build()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errCh := make(chan error, 1)
go func() {
errCh <- host.Run(ctx)
}()
select {
case err := <-errCh:
if err == nil {
t.Fatal("expected timeout error")
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for Run to return")
}
}
blockingHosted
type blockingHosted struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| release | chan struct{} | |
| stopped | atomic.Bool |
trackedCloser
type trackedCloser struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| closed | atomic.Bool |
blockingCloser
type blockingCloser struct
Methods
Returns
func (*blockingCloser) Close() error
{
close(c.started)
<-c.release
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| started | chan struct{} | |
| release | chan struct{} |
TestHost_StartupTimeoutDefersCleanupUntilStartReturns
Parameters
func TestHost_StartupTimeoutDefersCleanupUntilStartReturns(t *testing.T)
{
service := &blockingHosted{release: make(chan struct{})}
closer := &trackedCloser{}
builder := di.NewBuilder()
di.RegisterInstance(builder, closer)
container := builder.MustBuild()
var onStop atomic.Bool
host := NewBuilder().
UseContainer(container).
AddHostedService(service).
OnStop(func() { onStop.Store(true) }).
WithStartupTimeout(10 * time.Millisecond).
WithShutdownTimeout(20 * time.Millisecond).
Build()
err := host.Run(context.Background())
if err == nil {
t.Fatal("Run() accepted a hosted service that ignored startup cancellation")
}
if host.State() != HostStopping {
t.Fatalf("state = %s, want stopping", host.State())
}
if onStop.Load() || closer.closed.Load() {
t.Fatal("host finalized while the Start call was still running")
}
close(service.release)
deadline := time.Now().Add(time.Second)
for host.State() != HostStopped && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if host.State() != HostStopped {
t.Fatal("host did not finish deferred startup cleanup")
}
if !service.stopped.Load() || !onStop.Load() || !closer.closed.Load() {
t.Fatal("deferred startup cleanup did not stop and close all resources")
}
}
slowHosted
type slowHosted struct
Methods
Parameters
Returns
func (*slowHosted) Start(ctx context.Context) error
{
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
Parameters
Returns
func (*slowHosted) Stop(_ context.Context) error
{ return nil }
TestHost_State
Parameters
func TestHost_State(t *testing.T)
{
host := NewBuilder().Build()
if host.State() != HostStarting {
t.Errorf("initial state = %v, want Starting", host.State())
}
}
TestHost_AddHostedService
Parameters
func TestHost_AddHostedService(t *testing.T)
{
svc := &fakeHosted{}
host := &Host{}
host.AddHostedService(svc)
if len(host.hostedServices) != 1 {
t.Fatalf("expected 1 hosted service, got %d", len(host.hostedServices))
}
}
TestBackgroundServiceAdapter
Parameters
func TestBackgroundServiceAdapter(t *testing.T)
{
fake := &fakeSvc{}
adapter := &BackgroundServiceAdapter{Svc: fake}
if err := adapter.Start(context.Background()); err != nil {
t.Fatalf("Start() error = %v", err)
}
deadline := time.Now().Add(time.Second)
for !fake.running.Load() && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if !fake.running.Load() {
t.Fatal("background service did not start")
}
if err := adapter.Stop(context.Background()); err != nil {
t.Fatalf("Stop() error = %v", err)
}
if fake.running.Load() {
t.Fatal("background service did not stop")
}
}
failingBackground
type failingBackground struct
Methods
Parameters
Returns
func (*failingBackground) Execute(context.Context) error
{
return f.err
}
Fields
| Name | Type | Description |
|---|---|---|
| err | error |
TestHost_StopsWhenAdaptedBackgroundServiceFails
Parameters
func TestHost_StopsWhenAdaptedBackgroundServiceFails(t *testing.T)
{
want := errors.New("background failed")
host := NewBuilder().
AddHostedService(&BackgroundServiceAdapter{
Svc: &failingBackground{err: want},
}).
Build()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := host.Run(ctx)
if !errors.Is(err, want) {
t.Fatalf("Run() error = %v, want %v", err, want)
}
if host.State() != HostStopped {
t.Fatalf("state = %s, want stopped", host.State())
}
}
TestHostRecoversAdaptedBackgroundServicePanic
Parameters
func TestHostRecoversAdaptedBackgroundServicePanic(t *testing.T)
{
host := NewBuilder().
AddHostedService(&BackgroundServiceAdapter{Svc: &panicBackgroundService{}}).
Build()
err := host.Run(context.Background())
if err == nil || !strings.Contains(err.Error(), "background failed") {
t.Fatalf("Run() error = %v", err)
}
if host.State() != HostStopped {
t.Fatalf("state = %s, want stopped", host.State())
}
}
TestHost_WithHealthRegistry_ReadyEndpoint
Parameters
func TestHost_WithHealthRegistry_ReadyEndpoint(t *testing.T)
{
reg := health.NewRegistry()
reg.Register("db", &testChecker{status: health.StatusHealthy})
h := NewBuilder().
ConfigureWeb(func(s *web.Server) {}).
WithHealthRegistry(reg).
WithAddr(":0").
Build()
if h.Server == nil {
t.Fatal("Server should be set")
}
}
TestHost_RejectsHealthEndpointConflict
Parameters
func TestHost_RejectsHealthEndpointConflict(t *testing.T)
{
server := web.New()
if err := server.MapGet("/health/live", func(*web.Context) error {
return nil
}); err != nil {
t.Fatal(err)
}
registry := health.NewRegistry()
defer func() {
if recover() == nil {
t.Fatal("Build() ignored a health endpoint conflict")
}
}()
NewBuilder().
UseWeb(server).
WithHealthRegistry(registry).
Build()
}
TestHost_HealthEndpointRegistrationIsTransactional
Parameters
func TestHost_HealthEndpointRegistrationIsTransactional(t *testing.T)
{
server := web.New()
if err := server.MapGet("/health/ready", func(*web.Context) error {
return nil
}); err != nil {
t.Fatal(err)
}
func() {
defer func() {
if recover() == nil {
t.Fatal("Build() ignored readiness endpoint conflict")
}
}()
NewBuilder().
UseWeb(server).
WithHealthRegistry(health.NewRegistry()).
Build()
}()
for _, route := range server.Routes() {
if route.Path == "/health/live" {
t.Fatal("failed health batch partially registered liveness endpoint")
}
}
}
testChecker
type testChecker struct
Methods
Parameters
Returns
func (*testChecker) Check(_ context.Context) health.Report
{
return health.Report{Status: c.status}
}
Fields
| Name | Type | Description |
|---|---|---|
| status | health.Status |
TestHost_ShutdownTimeout
Parameters
func TestHost_ShutdownTimeout(t *testing.T)
{
b := NewBuilder().WithShutdownTimeout(5 * time.Second)
if b.shutdownTimeout != 5*time.Second {
t.Errorf("timeout = %v, want 5s", b.shutdownTimeout)
}
}
blockingBackground
type blockingBackground struct
Methods
Parameters
Returns
func (*blockingBackground) Execute(context.Context) error
{
close(s.started)
<-s.release
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| started | chan struct{} | |
| release | chan struct{} |
TestHost_ShutdownTimeoutDefersCleanupUntilServicesExit
Parameters
func TestHost_ShutdownTimeoutDefersCleanupUntilServicesExit(t *testing.T)
{
service := &blockingBackground{
started: make(chan struct{}),
release: make(chan struct{}),
}
closer := &trackedCloser{}
builder := di.NewBuilder()
di.RegisterInstance(builder, closer)
container := builder.MustBuild()
var onStop atomic.Bool
host := NewBuilder().
UseContainer(container).
AddService(service).
OnStop(func() { onStop.Store(true) }).
WithShutdownTimeout(20 * time.Millisecond).
Build()
ctx, cancel := context.WithCancel(context.Background())
runDone := make(chan error, 1)
go func() {
runDone <- host.Run(ctx)
}()
<-service.started
cancel()
err := <-runDone
if err == nil {
t.Fatal("Run() ignored shutdown timeout")
}
if host.State() != HostStopping {
t.Fatalf("state = %s, want stopping", host.State())
}
if onStop.Load() || closer.closed.Load() {
t.Fatal("host finalized while a background service was still running")
}
close(service.release)
deadline := time.Now().Add(time.Second)
for host.State() != HostStopped && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if host.State() != HostStopped || !onStop.Load() || !closer.closed.Load() {
t.Fatal("host did not finish deferred shutdown cleanup")
}
}
blockingStopHosted
type blockingStopHosted struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| started | atomic.Bool | |
| release | chan struct{} |
TestHost_ShutdownTimeoutDefersCleanupUntilHostedStops
Parameters
func TestHost_ShutdownTimeoutDefersCleanupUntilHostedStops(t *testing.T)
{
service := &blockingStopHosted{release: make(chan struct{})}
closer := &trackedCloser{}
builder := di.NewBuilder()
di.RegisterInstance(builder, closer)
container := builder.MustBuild()
var onStop atomic.Bool
host := NewBuilder().
UseContainer(container).
AddHostedService(service).
OnStop(func() { onStop.Store(true) }).
WithShutdownTimeout(20 * time.Millisecond).
Build()
ctx, cancel := context.WithCancel(context.Background())
runDone := make(chan error, 1)
go func() {
runDone <- host.Run(ctx)
}()
deadline := time.Now().Add(time.Second)
for !service.started.Load() && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
cancel()
if err := <-runDone; err == nil {
t.Fatal("Run() ignored a hosted service that blocked in Stop")
}
if host.State() != HostStopping {
t.Fatalf("state = %s, want stopping", host.State())
}
if onStop.Load() || closer.closed.Load() {
t.Fatal("host finalized while hosted Stop was still running")
}
close(service.release)
deadline = time.Now().Add(time.Second)
for host.State() != HostStopped && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if host.State() != HostStopped || !onStop.Load() || !closer.closed.Load() {
t.Fatal("host did not finish deferred hosted-service cleanup")
}
}
TestHost_ShutdownTimeoutIncludesContainerClose
Parameters
func TestHost_ShutdownTimeoutIncludesContainerClose(t *testing.T)
{
closer := &blockingCloser{
started: make(chan struct{}),
release: make(chan struct{}),
}
builder := di.NewBuilder()
di.RegisterInstance(builder, closer)
host := NewBuilder().
UseContainer(builder.MustBuild()).
WithShutdownTimeout(20 * time.Millisecond).
Build()
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := host.Run(ctx)
if err == nil {
t.Fatal("Run() ignored a blocking container closer")
}
if host.State() != HostStopping {
t.Fatalf("state = %s, want stopping", host.State())
}
select {
case <-closer.started:
default:
t.Fatal("container closer was not started")
}
close(closer.release)
deadline := time.Now().Add(time.Second)
for host.State() != HostStopped && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if host.State() != HostStopped {
t.Fatal("host did not finish after container closer returned")
}
}
TestHost_DefaultStartupTimeout
Parameters
func TestHost_DefaultStartupTimeout(t *testing.T)
{
b := NewBuilder()
if b.startupTimeout != 15*time.Second {
t.Errorf("default startup timeout = %v, want 15s", b.startupTimeout)
}
}
TestHostBuilderUsesLoopbackByDefault
Parameters
func TestHostBuilderUsesLoopbackByDefault(t *testing.T)
{
host := NewBuilder().UseWeb(web.New()).Build()
service, ok := host.services[0].(*webService)
if !ok {
t.Fatalf("service type = %T, want *webService", host.services[0])
}
if service.addr != "127.0.0.1:8080" {
t.Fatalf("default address = %q", service.addr)
}
}
TestHostBuilderConfiguresTLS
Parameters
func TestHostBuilderConfiguresTLS(t *testing.T)
{
host := NewBuilder().
UseWeb(web.New()).
WithTLS("certificate.pem", "key.pem").
Build()
service := host.services[0].(*webService)
if service.certFile != "certificate.pem" || service.keyFile != "key.pem" {
t.Fatalf("TLS files = %q, %q", service.certFile, service.keyFile)
}
}