Documentation
¶
Overview ¶
Example (Loader) ¶
Example_loader demonstrates how to use the loader package to load a footprint into a component lifecycle.
The footprint is a static description of the components to be loaded, and the bindings (i.e. linkages) between them.
package main
import (
"context"
"errors"
"fmt"
"reflect"
"time"
"gocloud.dev/pubsub"
"github.com/danielorbach/go-component"
"github.com/danielorbach/go-component/loader"
)
// Example_loader demonstrates how to use the loader package to load a footprint
// into a component lifecycle.
//
// The footprint is a static description of the components to be loaded, and the
// bindings (i.e. linkages) between them.
func main() {
// call loader.ParseFlags at the beginning of main() to parse the standard
// command-line flags for the loader package. it respects flags defined by the
// individual component descriptors.
loader.ParseFlags(SourceComponent, SinkComponent)
// call component.EntrypointProc to pass control of the main goroutine to the
// component package. this function will not return until its procedure and all
// its subcomponents have exited.
loader.EntrypointProc(func(l *component.L) {
// use the in-memory linker for this example
var bindings loader.SharedMemLinker
// call loader.Load with the current lifecycle to spawn a new subcomponent for
// each allocation claim in the footprint. the loader package will call the
// bootstrap function for each component, passing it the child lifecycle for the
// subcomponent.
loader.Load(loader.Footprint{
Name: "Example of a trivial source-sink footprint",
Metadata: "This footprint demonstrates how to use the loader package",
// each allocation claim describes an instance of a component to be loaded
Allocations: []*loader.Claim{
{
Component: SourceComponent,
// options are component-specific
Options: SourceOptions{Message: []byte("Hello Stewart")},
// bindings may be claim-specific, or shared between claims
// (as in this example) to link components together
Binding: &bindings,
},
{
Component: SinkComponent,
// some components have no options
Options: nil,
Binding: &bindings,
},
},
})
})
}
// Linkage is both the name of the aspect used by the source component, and
// the name of the interest used by the sink component.
const Linkage = "target-linkage"
// The SourceComponent is a source component that sends messages to the
// Linkage aspect.
var SourceComponent = &component.Descriptor{
// the name of the component is used to identify it in the footprint, and in
// command-line flags - it must be unique, a valid Go identifier.
Name: "source",
// the documentation of the component is used to generate the help text for the
// component's command-line flags - see the field's comment for more details on
// the format.
Doc: `
# Service source
source: continually publishes messages to the agreed-upon Linkage (aspect)
The source component sends messages every Interval duration on its aspect.
`,
// the loader package will call the bootstrap function for each instance of this
// component. this function is responsible for setting up any subcomponents
// (i.e., goroutines) and resources (i.e., files, sockets, etc.) that the
// component needs to operate. it returns when the component is running and ready
// - without waiting for it to stop.
Bootstrap: func(l *component.L, linker component.Linker, options any) error {
pub, err := linker.LinkAspect(l.Context(), Linkage)
if err != nil {
return fmt.Errorf("open aspect: %w", err)
}
l.CleanupBackground(pub.Shutdown)
// bootstrapping a component is a blocking operation; hence, any long-running
// operations must be performed in a subcomponent (i.e., a managed goroutine).
l.Go("pub", func(l *component.L) {
// options must be type-asserted to the type declared by this descriptor.
// it is safe to assume that the type is correct, otherwise a panic is
// appropriate.
options := options.(SourceOptions)
// standard Go lifecycle patterns (i.e., defer statements) are encouraged
// over lifecycle functions (i.e., l.CleanupBackground). the latter is
// more subject to change and is provided to support a new style of
// concurrent resource patterns.
t := time.NewTicker(Interval)
defer t.Stop()
// a long-running component usually has a select statement inside an infinite
// loop, as it spends most of its time waiting for event notifications until it
// is signaled to stop - either by the Stopping channel or the Context. lifecycle
// also provides GraceContext (see SourceComponent below) as a convenience for more
// complex use cases.
for {
select {
case <-t.C:
err := pub.Send(l.Context(), &pubsub.Message{Body: options.Message})
if err != nil {
l.Errorf("send: %w", err)
}
case <-l.Stopping():
// the earliest signal to stop a long-running component is the
// Stopping() channel. we must respect this signal and return
// as soon as possible.
return
case <-l.Context().Done():
// the latest signal to stop a long-running component is the
// Context().Done() channel - which is usually passed down to
// blocking functions (e.g., pub.Send) to signal them to abort.
return
}
}
})
return nil
},
// some loaders use the options-type to during construction of footprints from
// text to unmarshal the appropriate options for each component. the loader
// package does not use this field.
OptionsType: reflect.TypeOf(SourceOptions{}),
// a component declares the aspects it will publish. at this time, this field is
// not used by the loader package and provides for expressiveness.
Aspects: []string{Linkage},
// this component is not interested in other components, it is a pure source.
Interests: nil,
}
// it is common for components to share static configuration that is set
// separately from the component's options. this is usually done by defining a
// package-level variable and registering it as a command-line flag in the init
// function, as shown below.
var (
Interval time.Duration
)
func init() {
// the component's command-line flags are another way to configure the component.
// as opposed to its options, these flags are not part of the footprint and are
// constant for all instances of the component.
// the loader package will automatically add the flags to its CommandLine flags
// and parse them during ParseFlags.
// it is recommended to define the flags in an init function to avoid the need
// to create a new flag.FlagSet.
SourceComponent.Flags.DurationVar(&Interval, "interval", time.Second, "interval between echo notifications")
}
type SourceOptions struct {
Message []byte // data to send on the Linkage aspect
}
// The SinkComponent is a sink component that receives messages from the
// Linkage interest.
var SinkComponent = &component.Descriptor{
Name: "sink",
Doc: `
# Service sink
sink: continually subscribes to messages from the agreed-upon Linkage (interest)
The sink component receives simple messages from the source component and echoes
them to the lifecycle logger.
`,
Bootstrap: func(l *component.L, linker component.Linker, options any) error {
// establishing a link to another component is part of the initialization
// of the component. hence, it is done in the bootstrap function and its
// failure will cause the component to fail to start.
sub, err := linker.LinkInterest(l.Context(), Linkage)
if err != nil {
return fmt.Errorf("open interest: %w", err)
}
// the component must clean up any resources it has allocated during
// bootstrap. this is done by registering a cleanup function with the
// lifecycle. the cleanup function will be called after all the spawned
// subcomponents have been completed.
l.CleanupBackground(sub.Shutdown)
l.Go("sub", func(l *component.L) {
// lifecycle provides a convenient way to iterate forever, until
// the component is stopping. this is a common pattern for
// components that are interested in other components.
//
// the alternative is to use a naked for-loop, and check return
// abruptly when a Receive(l.Context()) call returns an error.
for l.Continue() {
// note the use of GraceContext() to ensure that the component
// gets a change to shut down in a timely manner.
msg, err := sub.Receive(l.GraceContext())
if err != nil {
// context.Cause will return ErrStopped if this context was
// canceled due to the component stopping.
if !errors.Is(context.Cause(l.GraceContext()), component.ErrStopped) {
l.Errorf("receive: %w", err)
}
continue
}
// ack messages received from interests as soon as possible,
// otherwise they may be received again.
msg.Ack()
// the lifecycle logger is always a uniform way to log messages from components
l.Log("ECHO", string(msg.Body))
}
})
return nil
},
// a component may have no options, in which case this field is nil.
OptionsType: nil,
// this component does not interest other components, it is a pure sink.
Aspects: nil,
// a component declares the interests it will subscribe to. at this time, this
// field is not used by the loader package and provides for expressiveness.
Interests: []string{Linkage},
}
Index ¶
- Variables
- func Entrypoint(main component.Procedure, opts ...component.Option)
- func EntrypointProc(main component.Proc, opts ...component.Option)
- func InvalidArgument(msg string)
- func IsEnabled(d *component.Descriptor) bool
- func Load(footprint Footprint, opts ...component.Option)
- func ParseFlags(descriptors ...*component.Descriptor)
- func ProgramName() string
- func Require(d *component.Descriptor, name string)
- func RequireCommandLine(name string)
- type Allocated
- type Claim
- type Footprint
- type InstanceId
- type MuxLinker
- type Reclaimed
- type SharedMemLinker
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // Health exposes the internal lifecycle of the loading process. // // The loader package takes control over the entire process, and is used like a // singleton. As such, it reports the health of the entire process. This // behaviour is supported by a single global variable. Health health HealthAddress string )
var ( // Debug is a set of single-letter flags: // // f show [f]acts as they are created // p disable [p]arallel execution of analyzers // s do additional [s]anity checks on fact types and serialization // t show [t]iming info (NB: use 'p' flag to avoid GC/scheduler noise) // v show [v]erbose logging // Debug = "" // Log files for optional performance tracing. CPUProfile, MemProfile, Trace string // LogLevel is the minimal level at which log records will be emitted. Any log // records below this level will be omitted. LogLevel slog.Level // Descriptors are the enabled components of the service Descriptors []*component.Descriptor )
var ( // CommandLine is the default set of command-line flags, parsed from os.Args. The // top-level function ParseFlags extends this set with flags for each of the // component's flags, then parses the extended set. Packages may use methods of // CommandLine directly to define their own flags such as Var() and so on. CommandLine = flag.NewFlagSet(ProgramName(), flag.ContinueOnError) )
Functions ¶
func InvalidArgument ¶
func InvalidArgument(msg string)
InvalidArgument alerts the user about a problem with the program's arguments and exits with a non-zero status.
func IsEnabled ¶
func IsEnabled(d *component.Descriptor) bool
func Load ¶
Load initializes the components and shared resources for the given footprint. This function guarantees that the initialization process is carried out only once, preventing re-initialization and potential conflicts. It configures the environment, applies the provided options, and starts the lifecycle of the components. Currently, only a single footprint is supported due to this decision.
In the future, the API of this package will be expanded to support multiple footprints. This change is probably backwards incompatible because it affects some decisions that make sense when loading only a single footprint. For example, the loader lifecycle stages that are exposed via Health.
func ParseFlags ¶
func ParseFlags(descriptors ...*component.Descriptor)
ParseFlags creates a flag for each of the component's flags, including (in "multi" mode) a flag named after the component, parses the flags, then filters and returns the list of descriptors enabled by flags.
func ProgramName ¶
func ProgramName() string
ProgramName returns the name of the program, as invoked by the user. If the program was invoked as "go run ...", the name of the file being run is returned.
func Require ¶
func Require(d *component.Descriptor, name string)
Require marks the component's named flag as required. If the flag is not set by the time ParseFlags is called, the program will exit with a non-zero status.
func RequireCommandLine ¶
func RequireCommandLine(name string)
RequireCommandLine marks the named global (i.e. on CommandLine flag-set) flag as required. If the flag is not set by the time ParseFlags is called, the program will exit with a non-zero status.
Types ¶
type Claim ¶
type Claim struct {
//InstanceId InstanceId
Component *component.Descriptor
Options any // component-specific configuration options
Binding component.Linker
// contains filtered or unexported fields
}
A Claim specifies the resources required to bootstrap and run an instance of a component. A single claim can be executed at most once.
func (*Claim) Exec ¶
Exec panics when called more than once for the same claim. It may panic while the first call to Exec has yet to return.
func (*Claim) Ready ¶
func (c *Claim) Ready() <-chan struct{}
Ready returns a channel that's closed when the Component loaded by this Claim is running and ready to serve its purpose.
The Component is ready when either component.Descriptor.Bootstrap returned successfully.
type Footprint ¶
type Footprint struct {
Name string // human-readable name (does not need to be unique)
Metadata string // human-readable description/summary/notes/comments
Identifier uuid.UUID
Revision int // linear (scalar) sequence number ordering different versions in time
Locations []string
Solution string
// An Allocation is a concrete resource claim that defines how a given Instance
// should be bootstrapped (allocated) from components (i.e. descriptors)
// available in given Locations, and their interconnections (i.e. Linkages -
// Aspect and Interest).
Allocations []*Claim
}
type InstanceId ¶
type MuxLinker ¶
type MuxLinker struct {
pubsub.URLMux
Aspects map[string]string // map[aspect]topic
Interests map[string]string // map[interest]topic
}
MuxLinker implements component.Linker over gocloud's pub-sub URLMux to support targets with multiple schemes (e.g., kafka://, mem://, etc.).
func (MuxLinker) LinkAspect ¶
func (MuxLinker) LinkInterest ¶
type SharedMemLinker ¶
SharedMemLinker implements component.Linker over shared-memory, in order to link components in the same address-space.
All topics are opened using the same URLOpener - in other words, use the same SharedMemLinker for all components that should share the same topics. The zero value is usable.
Establish a link between two components by using the same value as both the aspect and the interest - this linker does not support targeting the same data exchange (i.e. topic) with different keys, nor does it support targeting different topics for different components using the same aspect/interest.
func (*SharedMemLinker) LinkAspect ¶
func (*SharedMemLinker) LinkInterest ¶
func (l *SharedMemLinker) LinkInterest(ctx context.Context, interest string) (*pubsub.Subscription, error)