Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ The basic building block of a backend system is a component. A component is a de

Each component has the potential to **notify** other components about changes to its exposed _Aspect_. We say that those components have an _Interest_ to **track** the component's _Aspect_. As such, a component may have none, one or many _Aspects_ and _Interests_.

## Testing

A note for maintainers: the lifecycle suite runs under [testing/synctest][synctest] for deterministic control over concurrent startup, shutdown, and cleanup. When adding tests that exercise lifecycle concurrency, follow the existing tests in [`lifecycle_test.go`][lifecycle_test].

[synctest]: https://pkg.go.dev/testing/synctest
[lifecycle_test]: lifecycle_test.go

## Acknowledgments

Special thanks to [@ofektavor], [@yuvalmendelovich], [@marombracha], and [@arieltod] for your contributions and collaboration on this project.
Expand Down
34 changes: 34 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Package component provides building blocks for message-driven services whose
// startup, shutdown, and cleanup must happen in a controlled order.
//
// A component runs as a lifecycle, represented by [L]: a procedure with its own
// context that can spawn child lifecycles, register cleanup work, and react to a
// request to stop. [RunProc] starts one and blocks until it, its children, and
// their cleanup have all finished.
//
// # Testing with synctest
//
// The lifecycle's concurrency is exercised under [testing/synctest], which gives
// a test deterministic control over the goroutines a lifecycle starts. The L
// tests in lifecycle_test.go are the worked examples to follow when testing
// lifecycle behaviour.
//
// # Configuring a lifecycle
//
// [RunProc] and [Run] accept [Option] values that configure the lifecycle before
// it starts. The common ones are [WithName] to identify it in logs and traces,
// [WithContext] to root it in a parent context, [WithLogger] to direct its
// output, and [WithStopper] to hand it a channel whose closing asks the procedure
// to stop. That stop request is what [L.Continue] and [L.Stopping] report to the
// running code, so a procedure can wind down on its own terms.
//
// # Running a program
//
// A standalone program rarely calls [RunProc] itself. The
// [github.com/danielorbach/go-component/loader] subpackage provides Entrypoint,
// which installs a context, a logger, and an interrupt handler before running
// the procedure, so that pressing Ctrl-C or receiving a SIGTERM (as Kubernetes
// sends on shutdown) becomes the stop request the lifecycle observes. Reach for
// [RunProc] or [Run] when embedding a lifecycle inside a larger program or a
// test.
package component
51 changes: 51 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package component_test

import (
"fmt"

"github.com/danielorbach/go-component"
)

// A component that loops over units of work until the program asks it to stop.
// L.Continue reports false once the stop request arrives (delivered here through
// WithStopper, which a real program wires to an interrupt signal), so a work
// loop can check it between units and exit cleanly.
func ExampleL_Continue() {
// A real program closes this when it receives an interrupt signal.
shutdown := make(chan struct{})
go func() { close(shutdown) }()

component.RunProc(func(l *component.L) {
fmt.Println("working")
for l.Continue() {
// Do one unit of work here: handle a message, advance a job, poll
// a source. Keep each unit short so the loop reacts to the stop
// request promptly.
}
fmt.Println("stopped")
}, component.WithStopper(shutdown))

// Output:
// working
// stopped
}

// A component that has nothing to poll and simply blocks until the program asks
// it to stop. L.Stopping returns a channel that closes on the stop request
// (delivered here through WithStopper, which a real program wires to an
// interrupt signal), so the procedure waits on it and then returns to shut down.
func ExampleL_Stopping() {
// A real program closes this when it receives an interrupt signal.
shutdown := make(chan struct{})
go func() { close(shutdown) }()

component.RunProc(func(l *component.L) {
fmt.Println("serving")
<-l.Stopping()
fmt.Println("stopped")
}, component.WithStopper(shutdown))

// Output:
// serving
// stopped
}
19 changes: 19 additions & 0 deletions loader/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Package loader runs the components described by a footprint as one program.
//
// A [Footprint] is a static description of the components to start and the
// linkages between them. Each [Claim] in it names a [component.Descriptor], the
// options to bootstrap that component with, and the [component.Linker] that
// connects it to its peers. [Load] spawns one child lifecycle per claim and
// blocks until every component has bootstrapped or one of them fails.
//
// [Entrypoint] is the program's main entry point: it supplies a root context and
// a logger and installs a SIGINT/SIGTERM handler before running the procedure,
// so an operator's Ctrl-C or an orchestrator's shutdown signal becomes the
// request that stops the lifecycle. Call [ParseFlags] first to register and parse
// the command-line flags that the component descriptors declare.
//
// The package ships [component.Linker] implementations that wire components
// together: [MuxLinker] routes links through gocloud's pub-sub URL multiplexer,
// and [SharedMemLinker] connects components within a single process over shared
// memory.
package loader
8 changes: 8 additions & 0 deletions loader/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ import (
"github.com/danielorbach/go-component"
)

// EntrypointProc is [Entrypoint] for a procedure expressed as a [component.Proc].
func EntrypointProc(main component.Proc, opts ...component.Option) {
Entrypoint(main, opts...)
}

// Entrypoint runs main as the program's root lifecycle and blocks until it, its
// child lifecycles, and their cleanup have all finished.
//
// It supplies a background context and a logger that writes to standard error,
// then installs a signal handler: the first SIGINT or SIGTERM asks the lifecycle
// to stop gracefully, and a second SIGINT terminates it abruptly. The supplied
// options are applied after these defaults, so a caller may override them.
func Entrypoint(main component.Procedure, opts ...component.Option) {
opts = append(opts,
component.WithContext(context.Background()),
Expand Down