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 ¶
- Variables
- type HostOption
- type HostRuntime
- type IIdleReporter
- type ILazyService
- type IService
- type LazyService
- type LazyServiceOption
- type Scope
- func (scope *Scope) Cancel()
- func (scope *Scope) Child(owner string) (*Scope, error)
- func (scope *Scope) Close() error
- func (scope *Scope) Context() context.Context
- func (scope *Scope) Done() <-chan struct{}
- func (scope *Scope) Go(task func(ctx context.Context) error) error
- func (scope *Scope) GoOwner(name string, owner func(ctx context.Context) error) error
- func (scope *Scope) Goroutines() int64
- func (scope *Scope) Live() int64
- func (scope *Scope) Owner() string
- func (scope *Scope) Wait() error
- type ServiceFunc
- type ShutdownOrder
Constants ¶
This section is empty.
Variables ¶
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.
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.
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 ¶
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.
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 ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
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) Wait ¶
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 ¶
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 )