hosting API

hosting

package

API reference for the hosting package.

T
type

HostState

HostState represents the current lifecycle state of a Host.

app/hosting/host.go:21-21
type HostState int32
S
struct

Host

Host manages the lifecycle of the application.

app/hosting/host.go:53-67
type Host struct

Methods

Run
Method

Parameters

Returns

error
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

service HostedService

Returns

error
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

started int

Returns

<-chan error
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

error
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

error
func (*Host) closeContainer() error
{
	if h.Container == nil {
		return nil
	}
	return h.Container.Close()
}
Shutdown
Method

Parameters

Returns

error
func (*Host) Shutdown(_ context.Context) error
{
	h.mu.Lock()
	cancel := h.cancel
	h.mu.Unlock()
	if cancel != nil {
		cancel()
	}
	return nil
}
State
Method

Returns

func (*Host) State() HostState
{
	return HostState(h.state.Load())
}
OnStart
Method

Parameters

fn func()
func (*Host) OnStart(fn func())
{
	h.onStart = append(h.onStart, fn)
}
OnStop
Method

Parameters

fn func()
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
I
interface

BackgroundService

BackgroundService is a long-running service started in parallel.

app/hosting/host.go:70-72
type BackgroundService interface

Methods

Execute
Method

Parameters

Returns

error
func Execute(...)
I
interface

HostedService

HostedService is a managed lifecycle service with explicit Start/Stop.

app/hosting/host.go:75-78
type HostedService interface

Methods

Start
Method

Parameters

Returns

error
func Start(...)
Stop
Method

Parameters

Returns

error
func Stop(...)
S
struct
Implements: HostedService

BackgroundServiceAdapter

BackgroundServiceAdapter wraps a BackgroundService as a HostedService.

app/hosting/host.go:81-88
type BackgroundServiceAdapter struct

Methods

Start
Method

Parameters

Returns

error
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
Method

Completion reports when the adapted background service exits.

Returns

<-chan error
func (*BackgroundServiceAdapter) Completion() <-chan error
{
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.completion
}
Stop
Method

Parameters

Returns

error
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
F
function

safeLifecycleCall

Parameters

name
string
call
func() error

Returns

err
error
app/hosting/host.go:386-396
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()
}
S
struct

HostBuilder

HostBuilder provides a fluent API for constructing a Host.

app/hosting/host.go:425-439
type HostBuilder struct

Methods

Parameters

fn func(*di.Builder)

Returns

func (*HostBuilder) ConfigureServices(fn func(*di.Builder)) *HostBuilder
{
	if b.di == nil {
		b.di = di.NewBuilder()
	}
	fn(b.di)
	return b
}
ConfigureWeb
Method

Parameters

fn func(*web.Server)

Returns

func (*HostBuilder) ConfigureWeb(fn func(*web.Server)) *HostBuilder
{
	if b.web == nil {
		b.web = web.New()
	}
	fn(b.web)
	return b
}
UseWeb
Method

UseWeb attaches an existing web server to the host.

Parameters

server *web.Server

Returns

func (*HostBuilder) UseWeb(server *web.Server) *HostBuilder
{
	b.web = server
	return b
}
UseContainer
Method

UseContainer attaches an existing dependency container to the host.

Parameters

container *di.Container

Returns

func (*HostBuilder) UseContainer(container *di.Container) *HostBuilder
{
	b.container = container
	return b
}
AddService
Method

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
}
OnStart
Method

Parameters

fn func()

Returns

func (*HostBuilder) OnStart(fn func()) *HostBuilder
{
	b.onStart = append(b.onStart, fn)
	return b
}
OnStop
Method

Parameters

fn func()

Returns

func (*HostBuilder) OnStop(fn func()) *HostBuilder
{
	b.onStop = append(b.onStop, fn)
	return b
}
WithAddr
Method

Parameters

addr string

Returns

func (*HostBuilder) WithAddr(addr string) *HostBuilder
{
	b.webAddr = addr
	return b
}
WithTLS
Method

WithTLS configures the web host to serve HTTPS with the given certificate and key.

Parameters

certFile string
keyFile string

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
}
Build
Method

Returns

*Host
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
F
function

NewBuilder

NewBuilder creates a new HostBuilder.

Returns

app/hosting/host.go:442-446
func NewBuilder() *HostBuilder

{
	return &HostBuilder{
		startupTimeout: 15 * time.Second,
	}
}
S
struct
Implements: BackgroundService

webService

app/hosting/host.go:605-610
type webService struct

Methods

Execute
Method

Parameters

Returns

error
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
S
struct
Implements: BackgroundService

fakeSvc

app/hosting/host_test.go:16-18
type fakeSvc struct

Methods

Execute
Method

Parameters

Returns

error
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
F
function

TestBuilder_ConfigureServices

Parameters

app/hosting/host_test.go:27-41
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")
	}
}
F
function

TestBuilder_ConfigureWeb

Parameters

app/hosting/host_test.go:43-60
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))
	}
}
F
function

TestBuilder_UseWeb

Parameters

app/hosting/host_test.go:62-71
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))
	}
}
F
function

TestWebServiceHandlesImmediateCancellation

Parameters

app/hosting/host_test.go:73-80
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)
	}
}
F
function

TestBuilder_AddService

Parameters

app/hosting/host_test.go:82-88
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))
	}
}
F
function

TestBuilder_WithAddr

Parameters

app/hosting/host_test.go:90-95
func TestBuilder_WithAddr(t *testing.T)

{
	b := NewBuilder().WithAddr(":1234")
	if b.webAddr != ":1234" {
		t.Errorf("addr: got %q, want :1234", b.webAddr)
	}
}
F
function

TestHost_Lifecycle

Parameters

app/hosting/host_test.go:97-120
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")
	}
}
F
function

TestHost_RunIsOneShot

Parameters

app/hosting/host_test.go:122-151
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)
	}
}
S
struct

panicBackgroundService

app/hosting/host_test.go:153-153
type panicBackgroundService struct

Methods

Execute
Method

Parameters

Returns

error
func (*panicBackgroundService) Execute(context.Context) error
{
	panic("background failed")
}
F
function

TestHostRecoversBackgroundServicePanic

Parameters

app/hosting/host_test.go:159-168
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())
	}
}
F
function

TestHostOnStartPanicStillClosesContainer

Parameters

app/hosting/host_test.go:170-186
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")
	}
}
S
struct

fakeHosted

app/hosting/host_test.go:188-192
type fakeHosted struct

Methods

Start
Method

Parameters

Returns

error
func (*fakeHosted) Start(_ context.Context) error
{
	if f.startErr != nil {
		return f.startErr
	}
	f.started.Store(true)
	return nil
}
Stop
Method

Parameters

Returns

error
func (*fakeHosted) Stop(_ context.Context) error
{
	f.stopped.Store(true)
	return nil
}

Fields

Name Type Description
started atomic.Bool
stopped atomic.Bool
startErr error
F
function

TestHostedService_Lifecycle

Parameters

app/hosting/host_test.go:207-230
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")
	}
}
F
function

TestHostedService_StartupFailure

Parameters

app/hosting/host_test.go:232-240
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")
	}
}
F
function

TestHost_StartupTimeout

Parameters

app/hosting/host_test.go:242-265
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")
	}
}
S
struct

blockingHosted

app/hosting/host_test.go:267-270
type blockingHosted struct

Methods

Start
Method

Parameters

Returns

error
func (*blockingHosted) Start(context.Context) error
{
	<-s.release
	return nil
}
Stop
Method

Parameters

Returns

error
func (*blockingHosted) Stop(context.Context) error
{
	s.stopped.Store(true)
	return nil
}

Fields

Name Type Description
release chan struct{}
stopped atomic.Bool
S
struct

trackedCloser

app/hosting/host_test.go:282-284
type trackedCloser struct

Methods

Close
Method

Returns

error
func (*trackedCloser) Close() error
{
	c.closed.Store(true)
	return nil
}

Fields

Name Type Description
closed atomic.Bool
S
struct

blockingCloser

app/hosting/host_test.go:291-294
type blockingCloser struct

Methods

Close
Method

Returns

error
func (*blockingCloser) Close() error
{
	close(c.started)
	<-c.release
	return nil
}

Fields

Name Type Description
started chan struct{}
release chan struct{}
F
function

TestHost_StartupTimeoutDefersCleanupUntilStartReturns

Parameters

app/hosting/host_test.go:302-339
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")
	}
}
S
struct

slowHosted

app/hosting/host_test.go:341-341
type slowHosted struct

Methods

Start
Method

Parameters

Returns

error
func (*slowHosted) Start(ctx context.Context) error
{
	select {
	case <-time.After(5 * time.Second):
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}
Stop
Method

Parameters

Returns

error
func (*slowHosted) Stop(_ context.Context) error
{ return nil }
F
function

TestHost_State

Parameters

app/hosting/host_test.go:353-358
func TestHost_State(t *testing.T)

{
	host := NewBuilder().Build()
	if host.State() != HostStarting {
		t.Errorf("initial state = %v, want Starting", host.State())
	}
}
F
function

TestHost_AddHostedService

Parameters

app/hosting/host_test.go:360-367
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))
	}
}
F
function

TestBackgroundServiceAdapter

Parameters

app/hosting/host_test.go:369-389
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")
	}
}
S
struct

failingBackground

app/hosting/host_test.go:391-393
type failingBackground struct

Methods

Execute
Method

Parameters

Returns

error
func (*failingBackground) Execute(context.Context) error
{
	return f.err
}

Fields

Name Type Description
err error
F
function

TestHost_StopsWhenAdaptedBackgroundServiceFails

Parameters

app/hosting/host_test.go:399-416
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())
	}
}
F
function

TestHostRecoversAdaptedBackgroundServicePanic

Parameters

app/hosting/host_test.go:418-430
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())
	}
}
F
function

TestHost_WithHealthRegistry_ReadyEndpoint

Parameters

app/hosting/host_test.go:432-445
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")
	}
}
F
function

TestHost_RejectsHealthEndpointConflict

Parameters

app/hosting/host_test.go:447-465
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()
}
F
function

TestHost_HealthEndpointRegistrationIsTransactional

Parameters

app/hosting/host_test.go:467-492
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")
		}
	}
}
S
struct

testChecker

app/hosting/host_test.go:494-496
type testChecker struct

Methods

Check
Method

Parameters

Returns

func (*testChecker) Check(_ context.Context) health.Report
{
	return health.Report{Status: c.status}
}

Fields

Name Type Description
status health.Status
F
function

TestHost_ShutdownTimeout

Parameters

app/hosting/host_test.go:502-507
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)
	}
}
S
struct

blockingBackground

app/hosting/host_test.go:509-512
type blockingBackground struct

Methods

Execute
Method

Parameters

Returns

error
func (*blockingBackground) Execute(context.Context) error
{
	close(s.started)
	<-s.release
	return nil
}

Fields

Name Type Description
started chan struct{}
release chan struct{}
F
function

TestHost_ShutdownTimeoutDefersCleanupUntilServicesExit

Parameters

app/hosting/host_test.go:520-563
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")
	}
}
S
struct

blockingStopHosted

app/hosting/host_test.go:565-568
type blockingStopHosted struct

Methods

Start
Method

Parameters

Returns

error
func (*blockingStopHosted) Start(context.Context) error
{
	s.started.Store(true)
	return nil
}
Stop
Method

Parameters

Returns

error
func (*blockingStopHosted) Stop(context.Context) error
{
	<-s.release
	return nil
}

Fields

Name Type Description
started atomic.Bool
release chan struct{}
F
function

TestHost_ShutdownTimeoutDefersCleanupUntilHostedStops

Parameters

app/hosting/host_test.go:580-622
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")
	}
}
F
function

TestHost_ShutdownTimeoutIncludesContainerClose

Parameters

app/hosting/host_test.go:624-659
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")
	}
}
F
function

TestHost_DefaultStartupTimeout

Parameters

app/hosting/host_test.go:661-666
func TestHost_DefaultStartupTimeout(t *testing.T)

{
	b := NewBuilder()
	if b.startupTimeout != 15*time.Second {
		t.Errorf("default startup timeout = %v, want 15s", b.startupTimeout)
	}
}
F
function

TestHostBuilderUsesLoopbackByDefault

Parameters

app/hosting/host_test.go:668-677
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)
	}
}
F
function

TestHostBuilderConfiguresTLS

Parameters

app/hosting/host_test.go:679-688
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)
	}
}