runtime

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package runtime is the process runtime: it owns the lifetimes every goroutine in a CSF process starts under.

A process has exactly one HostRuntime, built by its main function, which owns the root lifetime. Services mount into it; each mounted service receives its own Scope. A scope has one owner, one cancellation and one join: when it ends, every goroutine it started has returned and every error they reported is collected.

A scope is the easiest way to own a goroutine, not the only permitted one. Services may start goroutines themselves; whoever starts one is responsible for its cleanup (cancellation and a join), and when goroutines coordinate across services, the code that wires them together owns that lifecycle. House rule CS-15 locates goroutines that show no such owner.

A service mounted through NewLazyService starts on its first use instead of at mount, on a child scope that is canceled and joined after an idle timeout and started afresh on the next use.

Index

Constants

This section is empty.

Variables

View Source
var ErrLazyServiceNotStarted = errors.New("runtime: lazy service has not been started")

ErrLazyServiceNotStarted is returned by LazyService.Use before the lazy service has been mounted, that is, before its own Start recorded a parent.

View Source
var ErrLazyServiceStopped = errors.New("runtime: lazy service has stopped")

ErrLazyServiceStopped is returned by LazyService.Use once the scope the lazy service was mounted on has been canceled, or after the wrapped service failed on its own: there is nothing left to start it under.

View Source
var ErrScopeClosed = errors.New("runtime: scope is closed")

ErrScopeClosed is returned when a goroutine or child scope is requested from a scope that has already begun joining.

Functions

This section is empty.

Types

type HostOption

type HostOption func(options *hostOptions)

HostOption configures a HostRuntime.

func WithHostName

func WithHostName(name string) HostOption

WithHostName names the runtime in its lifecycle logs.

func WithLogger

func WithLogger(logger *slog.Logger) HostOption

WithLogger receives one structured record per lifecycle transition: each service started, shutdown requested, each service joined.

func WithShutdownOrder

func WithShutdownOrder(order ShutdownOrder) HostOption

WithShutdownOrder chooses how services stop. Unknown orders are rejected by NewHostRuntime.

type HostRuntime

type HostRuntime struct {
	// contains filtered or unexported fields
}

HostRuntime is the one runtime of one process. Its main function builds it, grants capabilities to the services it constructs, mounts them, and calls HostRuntime.Run with the root context it owns.

Startup is ordered: services start in the order they were mounted, so a service may depend on everything mounted before it. Shutdown is, by default, the reverse: the last service mounted is canceled and joined first, so a dependency outlives everything that uses it (ReverseMountOrder); WithShutdownOrder can choose Concurrent instead. A mounted service's scope is not canceled by the root context directly — the runtime cancels the scopes itself — which is what makes the chosen order real rather than a race.

func NewHostRuntime

func NewHostRuntime(options ...HostOption) (*HostRuntime, error)

NewHostRuntime validates its options and returns an empty runtime.

func (*HostRuntime) Mount

func (host *HostRuntime) Mount(name string, service IService) error

Mount adds a service. It must be called before Run; the mount order is the startup order and, by default, the reverse of the shutdown order.

func (*HostRuntime) Ready

func (host *HostRuntime) Ready() bool

Ready reports whether every service has started and shutdown has not begun. It is the readiness probe's answer.

func (*HostRuntime) Run

func (host *HostRuntime) Run(ctx context.Context) error

Run starts every mounted service in order, then blocks until ctx is canceled or a service fails, then stops every started service in reverse order and joins it. It returns the services' collected errors; a clean shutdown returns nil. Run may be called once.

type IIdleReporter

type IIdleReporter interface {
	Idle() bool
}

IIdleReporter is optionally implemented by a service wrapped in a LazyService. When no caller holds a lease and the idle timeout has elapsed, the wrapper asks it before retiring the run: a service with work still in hand — queued messages, a blocked receiver — reports false and is asked again one idle timeout later. A service that does not implement it is idle whenever no caller holds a lease.

type ILazyService

type ILazyService[T IService] interface {
	IService
	Use(ctx context.Context, call func(service T) error) error
}

ILazyService is a service started on first use rather than at mount.

It is mounted like any IService; its Start only records the scope it was mounted on. The first ILazyService.Use starts T on a child of that scope and holds a lease for the duration of the call. When the last lease is released and the idle timeout elapses, the child scope is canceled and joined; the next Use starts T again on a new child scope.

The contract on T: Start may be called more than once, each time on a new scope, and only after every goroutine of the previous scope has returned. Whatever T must keep between runs it keeps outside the goroutines its scope joins. Start errors surface at use time, to the caller of Use, which is why lazy start is a composer's visible choice rather than a default.

type IService

type IService interface {
	Start(scope *Scope) error
}

IService is what mounts into a HostRuntime. Start runs once, in mount order, and receives the service's own scope: every goroutine the service needs is started through that scope, and Start returns as soon as they are started. The service stops when its scope is canceled, and performs its own cleanup before its goroutines return.

type LazyService

type LazyService[T IService] struct {
	// contains filtered or unexported fields
}

LazyService wraps T so it starts on first use and stops when idle. See ILazyService for the lifecycle and the contract on T.

func NewLazyService

func NewLazyService[T IService](service T, options ...LazyServiceOption) (*LazyService[T], error)

NewLazyService validates its options and wraps service without starting it. WithIdleTimeout is required.

func (*LazyService[T]) Start

func (lazy *LazyService[T]) Start(scope *Scope) error

Start records the scope the lazy service is mounted on. It starts nothing: T starts on the first LazyService.Use, on a child of this scope, and the scope's cancellation stops it like any mounted service.

func (*LazyService[T]) Use

func (lazy *LazyService[T]) Use(ctx context.Context, call func(service T) error) error

Use runs call with T, starting T first if it is not running, and holds a lease for the duration of the call so the run cannot be retired under it. The call's error is returned as is.

type LazyServiceOption

type LazyServiceOption func(options *lazyOptions) error

LazyServiceOption configures a LazyService; NewLazyService validates the whole set before building anything.

func WithIdleTimeout

func WithIdleTimeout(idle time.Duration) LazyServiceOption

WithIdleTimeout is how long a lazy service may go without a lease before its run is retired. It is required, and it must be positive.

func WithLazyClock

func WithLazyClock(now func() time.Time) LazyServiceOption

WithLazyClock replaces the clock that measures idleness. The idle check still wakes on the real clock; a test that freezes this one holds the service running, and advancing it lets the next check retire the run.

func WithLazyName

func WithLazyName(name string) LazyServiceOption

WithLazyName names the child scope each run starts on, under the scope the lazy service is mounted on.

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

Scope is one explicit lifetime with one owner, one cancellation and one completion. Cancellation requests shutdown; Scope.Wait returns once every goroutine started through the scope, and every child scope, has exited.

A goroutine that returns an error cancels its scope, and the scope reports the failure to whoever owns it — its parent scope, or the HostRuntime for a mounted service — so one failed owner stops the process instead of leaving it half-running. Errors are collected rather than racing for first place: Scope.Wait returns all of them joined.

The zero value is not usable; scopes come from NewScope, Scope.Child and HostRuntime.Mount.

func NewScope

func NewScope(ctx context.Context, owner string) *Scope

NewScope returns a standalone scope whose lifetime is bounded by ctx. It is the scope a test or a one-shot tool uses; a long-running process gets its scopes from a HostRuntime instead.

func (*Scope) Cancel

func (scope *Scope) Cancel()

Cancel asks every goroutine in the scope, and in its children, to stop. It does not wait.

func (*Scope) Child

func (scope *Scope) Child(owner string) (*Scope, error)

Child returns a scope whose lifetime is nested in this one: canceling the parent cancels the child, and the parent's join waits for the child's. A child's errors are its parent's errors.

func (*Scope) Close

func (scope *Scope) Close() error

Close cancels the scope and waits for it to join.

func (*Scope) Context

func (scope *Scope) Context() context.Context

Context is canceled when the scope is asked to stop. Values the root context carried are preserved.

func (*Scope) Done

func (scope *Scope) Done() <-chan struct{}

Done is closed when the scope has been asked to stop.

func (*Scope) Go

func (scope *Scope) Go(task func(ctx context.Context) error) error

Go starts task on a new goroutine owned by this scope. A task may finish at any time; returning nil before the scope is canceled is a normal completion. A non-nil error cancels the scope and is reported by Scope.Wait. context.Canceled returned after the scope was canceled is not a failure.

func (*Scope) GoOwner

func (scope *Scope) GoOwner(name string, owner func(ctx context.Context) error) error

GoOwner starts owner on a new goroutine that must last the whole scope: a server loop, a watcher, a datum's owning goroutine. Returning — even with a nil error — before the scope is canceled is reported as a failure, because a process whose owner silently stopped is not running.

func (*Scope) Goroutines

func (scope *Scope) Goroutines() int64

Goroutines reports how many goroutines this scope has started, counting a child scope's join as one. After Scope.Wait returns every one of them has exited.

func (*Scope) Live

func (scope *Scope) Live() int64

Live reports how many goroutines this scope started that have not yet returned, counting a running child scope's join as one. Unlike Scope.Goroutines, which only grows, Live falls back as goroutines exit: it is how a caller observes that a retired lifetime, such as a LazyService's idle child, really joined.

func (*Scope) Owner

func (scope *Scope) Owner() string

Owner names the scope in logs and errors.

func (*Scope) Wait

func (scope *Scope) Wait() error

Wait blocks until every goroutine the scope started has returned and returns their collected errors. It does not cancel the scope; call Scope.Close to do both. Wait may be called more than once and from more than one goroutine.

type ServiceFunc

type ServiceFunc func(scope *Scope) error

ServiceFunc adapts a function to IService.

func (ServiceFunc) Start

func (start ServiceFunc) Start(scope *Scope) error

Start calls the function.

type ShutdownOrder

type ShutdownOrder int

ShutdownOrder is how a HostRuntime stops its services. Startup is always the mount order; only the stop is a choice.

const (
	// ReverseMountOrder, the default, cancels and joins one service at a time,
	// last mounted first, so every dependency outlives what uses it.
	ReverseMountOrder ShutdownOrder = iota
	// Concurrent cancels every service's scope at once and joins them all.
	// It is for services with no shutdown dependencies on one another, where
	// the slowest drain, not the sum of them, should bound the stop.
	Concurrent
)

Directories

Path Synopsis
Package config is the config capability: the one place in a CSF process that reads the process environment.
Package config is the config capability: the one place in a CSF process that reads the process environment.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL