hosting API

hosting

package

API reference for the hosting package.

T
type

HostState

HostState represents the current lifecycle state of a Host.

pkg/hosting/host.go:19-19
type HostState int32
S
struct

Host

Host manages the lifecycle of the application.

pkg/hosting/host.go:48-61
type Host struct

Methods

Run
Method

Parameters

Returns

error
func (*Host) Run(ctx context.Context) error
{
	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 {
		fn()
	}

	startupCtx, startupCancel := context.WithTimeout(ctx, h.startupTimeout)
	defer startupCancel()

	for _, svc := range h.hostedServices {
		if err := svc.Start(startupCtx); err != nil {
			startupCancel()
			h.shutdownHosted(ctx)
			return fmt.Errorf("hosted service start failed: %w", err)
		}
	}

	var wg sync.WaitGroup
	errCh := make(chan error, len(h.services))

	for _, svc := range h.services {
		s := svc
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := s.Execute(ctx); err != nil {
				errCh <- err
			}
		}()
	}

	h.state.Store(int32(HostRunning))

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

	var firstErr error
	select {
	case <-sigCh:
	case <-ctx.Done():
	case err := <-errCh:
		firstErr = err
	}

	cancel()

	h.state.Store(int32(HostStopping))

	h.shutdownHosted(ctx)

	stopped := make(chan struct{})
	go func() {
		wg.Wait()
		close(stopped)
	}()

	timeout := h.ShutdownTimeout
	if timeout == 0 {
		timeout = 30 * time.Second
	}

	select {
	case <-stopped:
	case <-time.After(timeout):
	}

	for _, fn := range h.onStop {
		fn()
	}

	h.state.Store(int32(HostStopped))

	if firstErr != nil {
		return firstErr
	}
	return nil
}

Parameters

func (*Host) shutdownHosted(_ context.Context)
{
	stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	for i := len(h.hostedServices) - 1; i >= 0; i-- {
		h.hostedServices[i].Stop(stopCtx)
	}
}
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 *srv.Server
HealthRegistry *health.Registry
cancel context.CancelFunc
mu sync.RWMutex
state atomic.Int32
ShutdownTimeout time.Duration
startupTimeout time.Duration
I
interface

BackgroundService

BackgroundService is a long-running service started in parallel.

pkg/hosting/host.go:64-66
type BackgroundService interface

Methods

Execute
Method

Parameters

Returns

error
func Execute(...)
I
interface

HostedService

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

pkg/hosting/host.go:69-72
type HostedService interface

Methods

Start
Method

Parameters

Returns

error
func Start(...)
Stop
Method

Parameters

Returns

error
func Stop(...)
S
struct

BackgroundServiceAdapter

BackgroundServiceAdapter wraps a BackgroundService as a HostedService.

pkg/hosting/host.go:75-77
type BackgroundServiceAdapter struct

Methods

Start
Method

Parameters

Returns

error
func (*BackgroundServiceAdapter) Start(ctx context.Context) error
{
	errCh := make(chan error, 1)
	go func() {
		errCh <- a.Svc.Execute(ctx)
	}()
	select {
	case err := <-errCh:
		return err
	case <-ctx.Done():
		return ctx.Err()
	}
}
Stop
Method

Parameters

Returns

error
func (*BackgroundServiceAdapter) Stop(_ context.Context) error
{
	return nil
}

Fields

Name Type Description
Svc BackgroundService
S
struct

HostBuilder

HostBuilder provides a fluent API for constructing a Host.

pkg/hosting/host.go:218-229
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(*srv.Server)

Returns

func (*HostBuilder) ConfigureWeb(fn func(*srv.Server)) *HostBuilder
{
	if b.web == nil {
		b.web = srv.New()
	}
	fn(b.web)
	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
}

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,
	}

	if b.di != nil {
		c, err := b.di.Build()
		if err != nil {
			panic(err)
		}
		h.Container = c
	}
	if b.web != nil {
		addr := b.webAddr
		if addr == "" {
			addr = ":8080"
		}
		s := &webService{server: b.web, container: h.Container, addr: addr}
		h.services = append(h.services, s)
		h.Server = b.web
	}

	if h.HealthRegistry != nil && h.Server != nil {
		h.Server.MapGet("/health/live", func(ctx *srv.Context) error {
			return ctx.JSON(200, map[string]string{"status": "alive"})
		})
		h.Server.MapGet("/health/ready", func(ctx *srv.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,
			})
		})
	}

	return h
}

Fields

Name Type Description
services []BackgroundService
hostedServices []HostedService
onStart []func()
onStop []func()
di *di.Builder
web *srv.Server
webAddr string
shutdownTimeout time.Duration
startupTimeout time.Duration
healthRegistry *health.Registry
F
function

NewBuilder

NewBuilder creates a new HostBuilder.

Returns

pkg/hosting/host.go:232-236
func NewBuilder() *HostBuilder

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

webService

pkg/hosting/host.go:352-356
type webService struct

Methods

Execute
Method

Parameters

Returns

error
func (*webService) Execute(ctx context.Context) error
{
	errCh := make(chan error, 1)
	go func() {
		errCh <- w.server.ListenAndServe(w.addr)
	}()
	select {
	case <-ctx.Done():
		w.server.Shutdown(context.Background())
		return nil
	case err := <-errCh:
		return err
	}
}

Fields

Name Type Description
server *srv.Server
container *di.Container
addr string
S
struct
Implements: BackgroundService

fakeSvc

pkg/hosting/host_test.go:14-16
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

pkg/hosting/host_test.go:25-39
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

pkg/hosting/host_test.go:41-58
func TestBuilder_ConfigureWeb(t *testing.T)

{
	h := NewBuilder().
		ConfigureWeb(func(app *srv.Server) {
			app.MapGet("/test", func(c *srv.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_AddService

Parameters

pkg/hosting/host_test.go:60-66
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

pkg/hosting/host_test.go:68-73
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

pkg/hosting/host_test.go:75-98
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")
	}
}
S
struct

fakeHosted

pkg/hosting/host_test.go:102-106
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

pkg/hosting/host_test.go:121-144
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

pkg/hosting/host_test.go:146-154
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

pkg/hosting/host_test.go:156-179
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

slowHosted

pkg/hosting/host_test.go:181-181
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

pkg/hosting/host_test.go:193-198
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

pkg/hosting/host_test.go:200-207
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

pkg/hosting/host_test.go:209-228
func TestBackgroundServiceAdapter(t *testing.T)

{
	fake := &fakeSvc{}
	adapter := &BackgroundServiceAdapter{Svc: fake}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	errCh := make(chan error, 1)
	go func() {
		errCh <- adapter.Start(ctx)
	}()

	time.Sleep(20 * time.Millisecond)
	cancel()

	err := <-errCh
	if err != nil && err != context.Canceled {
		t.Errorf("unexpected error: %v", err)
	}
}
F
function

TestHost_WithHealthRegistry_ReadyEndpoint

Parameters

pkg/hosting/host_test.go:230-243
func TestHost_WithHealthRegistry_ReadyEndpoint(t *testing.T)

{
	reg := health.NewRegistry()
	reg.Register("db", &testChecker{status: health.StatusHealthy})

	h := NewBuilder().
		ConfigureWeb(func(s *srv.Server) {}).
		WithHealthRegistry(reg).
		WithAddr(":0").
		Build()

	if h.Server == nil {
		t.Fatal("Server should be set")
	}
}
S
struct

testChecker

pkg/hosting/host_test.go:245-247
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

pkg/hosting/host_test.go:253-258
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)
	}
}
F
function

TestHost_DefaultStartupTimeout

Parameters

pkg/hosting/host_test.go:260-265
func TestHost_DefaultStartupTimeout(t *testing.T)

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