Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
private/mud: service lifecycle utilities
Change-Id: I3a05a4f84f414a5fed400f6e2ec5166551bb0fb4
- Loading branch information
Showing
8 changed files
with
1,074 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# mud | ||
|
||
mud is a package for lazily starting up a large set of services and chores. | ||
The `mud` package name comes from a common architectural pattern called "Big Ball of Mud". | ||
|
||
You can think about mud as a very huge map of service instances: `map[type]component`. | ||
Where component includes the singleton instance and functions to initialize the singleton and run/close them. | ||
|
||
Components can also depend on each other, and there are helper function to filter the required components (and/or | ||
initialize/start them). | ||
|
||
Compared to other similar libraries, like https://github.com/uber-go/fx or https://github.com/uber-go/dig, mud is just a | ||
very flexible framework, it wouldn't like to restrict the usage. Therefore advanced workflows also can be implemented | ||
with filtering different graphs and using them. | ||
|
||
Users of this library has more power (and more responsibility). | ||
|
||
## Getting started | ||
|
||
You can create the instance registry with: | ||
|
||
``` | ||
mud := mud.NewBall() | ||
``` | ||
|
||
Register a new component: | ||
|
||
``` | ||
Provide[your.Service](ball, func() your.Service { | ||
return your.NewService() | ||
}) | ||
``` | ||
|
||
Now, your component is registered, but not yet initialized. You should select some of the services to Init / run them: | ||
|
||
``` | ||
err := mud.ForEach(ball, mud.Initialize(context.TODO())) | ||
if err != nil { | ||
panic(err) | ||
} | ||
``` | ||
|
||
Now your component is initialized: | ||
|
||
``` | ||
fmt.Println(mud.Find(ball, mud.All)[0].Instance()) | ||
``` | ||
|
||
This one selected the first component (we registered only one), but you can also use different selectors. This one | ||
selects the components by type. | ||
|
||
``` | ||
fmt.Println(mud.Find(ball, mud.Select[your.Service](ball))[0].Instance()) | ||
``` | ||
|
||
Or, of you are sure, it's there: | ||
|
||
``` | ||
fmt.Println(mud.MustLookup[your.Service](ball)) | ||
``` | ||
|
||
## Dependencies and dependency injection | ||
|
||
Dependencies are automatically injected. Let's say you have two structs: | ||
|
||
``` | ||
type Service struct { | ||
} | ||
func NewService() Service { | ||
return Service{} | ||
} | ||
type Endpoint struct { | ||
Service Service | ||
} | ||
func NewEndpoint(service Service) Endpoint { | ||
return Endpoint{ | ||
Service: service, | ||
} | ||
} | ||
``` | ||
|
||
Now you can register both: | ||
|
||
``` | ||
mud.Provide[your.Service](ball, your.NewService) | ||
mud.Provide[your.Endpoint](ball, your.NewEndpoint) | ||
``` | ||
|
||
When you initialize the Endpoint, Service will be injected (if the instance is available!!!): | ||
|
||
``` | ||
err := mud.MustDo[your.Service](ball, mud.Initialize(context.TODO())) | ||
if err != nil { | ||
panic(err) | ||
} | ||
err = mud.MustDo[your.Endpoint](ball, mud.Initialize(context.TODO())) | ||
if err != nil { | ||
panic(err) | ||
} | ||
``` | ||
|
||
But instead of initializing manually, you can also just ask what you need, and initialize everything in the right order | ||
|
||
``` | ||
err := mud.ForEachDependency(ball, mud.Select[your.Endpoint](ball), mud.Initialize(context.TODO()), mud.All) | ||
``` | ||
|
||
## Views | ||
|
||
Views are useful when you already have sg. registered, but you would like to make it fully or partially available under | ||
different type: | ||
|
||
``` | ||
mud.Provide[satellite.DB](ball, OpenSatelliteDB) | ||
mud.View[satellite.DB, gracefulexit.DB](ball, satellite.DB.GracefulExit) | ||
``` | ||
|
||
This registers a `satellite.DB` (first line) and a `gracefulexit.DB` (second line). And if `gracefulexit.DB` is needed | ||
for injection, it will call the function to get it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// Copyright (C) 2024 Storj Labs, Inc. | ||
// See LICENSE for copying information. | ||
|
||
package mud | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"strings" | ||
"time" | ||
|
||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
// StageName is the unique identifier of the stages (~lifecycle events). | ||
type StageName string | ||
|
||
// Component manages the lifecycle of a singleton Golang struct. | ||
type Component struct { | ||
target reflect.Type | ||
|
||
instance any | ||
|
||
// Requirements are other components which is used by this component. | ||
// All requirements will be initialized/started before creating/running the component. | ||
requirements []reflect.Type | ||
|
||
create *Stage | ||
|
||
run *Stage | ||
|
||
close *Stage | ||
|
||
tags []any | ||
} | ||
|
||
// Name returns with the human friendly name of the component. | ||
func (c *Component) Name() string { | ||
return c.target.String() | ||
} | ||
|
||
// ID is the unque identifier of the component. | ||
func (c *Component) ID() string { | ||
return fullyQualifiedTypeName(c.target) | ||
} | ||
|
||
// Init initializes the internal singleton instance. | ||
func (c *Component) Init(ctx context.Context) error { | ||
if c.instance != nil { | ||
return nil | ||
} | ||
c.create.started = time.Now() | ||
err := c.create.run(nil, ctx) | ||
c.create.finished = time.Now() | ||
return err | ||
} | ||
|
||
// Run executes the Run stage function. | ||
func (c *Component) Run(ctx context.Context, eg *errgroup.Group) error { | ||
if c.run == nil || !c.run.started.IsZero() { | ||
return nil | ||
} | ||
if c.instance == nil { | ||
return nil | ||
} | ||
|
||
if c.run.background { | ||
eg.Go(func() error { | ||
c.run.started = time.Now() | ||
err := c.run.run(c.instance, ctx) | ||
c.run.finished = time.Now() | ||
return err | ||
}) | ||
return nil | ||
} else { | ||
c.run.started = time.Now() | ||
err := c.run.run(c.instance, ctx) | ||
c.run.finished = time.Now() | ||
return err | ||
} | ||
} | ||
|
||
// Close calls the Close stage function. | ||
func (c *Component) Close(ctx context.Context) error { | ||
if c.close == nil || c.close.run == nil || !c.close.started.IsZero() || c.instance == nil { | ||
return nil | ||
} | ||
c.close.started = time.Now() | ||
err := c.close.run(c.instance, ctx) | ||
c.close.finished = time.Now() | ||
return err | ||
} | ||
|
||
// String returns with a string representation of the component. | ||
func (c *Component) String() string { | ||
out := c.target.String() | ||
out += stageStr(c.create, "i") | ||
out += stageStr(c.run, "r") | ||
out += stageStr(c.close, "c") | ||
return out | ||
} | ||
|
||
func stageStr(stage *Stage, s string) string { | ||
if stage == nil { | ||
return "_" | ||
} | ||
if stage.started.IsZero() { | ||
return strings.ToLower(s) | ||
} | ||
return strings.ToUpper(s) | ||
} | ||
|
||
func (c *Component) addRequirement(in reflect.Type) { | ||
for _, req := range c.requirements { | ||
if req == in { | ||
return | ||
} | ||
} | ||
c.requirements = append(c.requirements, in) | ||
} | ||
|
||
// Instance returns the singleton instance of the component. Can be null, if not yet initialized. | ||
func (c *Component) Instance() any { | ||
return c.instance | ||
} | ||
|
||
// GetTarget returns with type, which is used as n identifier in mud. | ||
func (c *Component) GetTarget() reflect.Type { | ||
return c.target | ||
} | ||
|
||
// Stage represents a function which should be called on the component at the right time (like start, stop, init). | ||
type Stage struct { | ||
run func(any, context.Context) error | ||
|
||
// should be executed in the background or not. | ||
background bool | ||
|
||
started time.Time | ||
|
||
finished time.Time | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Copyright (C) 2024 Storj Labs, Inc. | ||
// See LICENSE for copying information. | ||
|
||
package mud | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
) | ||
|
||
// Optional tag is used to mark components which may not required. | ||
type Optional struct{} | ||
|
||
// Find selects components matching the selector. | ||
func Find(ball *Ball, selector ComponentSelector) (result []*Component) { | ||
for _, c := range ball.registry { | ||
if selector(c) { | ||
result = append(result, c) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
// FindSelectedWithDependencies selects components matching the selector, together with all the dependencies. | ||
func FindSelectedWithDependencies(ball *Ball, selector ComponentSelector) (result []*Component) { | ||
dependencies := map[reflect.Type]struct{}{} | ||
for _, component := range ball.registry { | ||
if selector(component) { | ||
collectDependencies(ball, component, dependencies) | ||
} | ||
} | ||
for k := range dependencies { | ||
result = append(result, mustLookupByType(ball, k)) | ||
} | ||
return filterComponents(sortedComponents(ball), result) | ||
} | ||
|
||
func collectDependencies(ball *Ball, c *Component, result map[reflect.Type]struct{}) { | ||
// don't check it again | ||
for k := range result { | ||
if c.target == k { | ||
return | ||
} | ||
} | ||
|
||
// don't check it again | ||
result[c.target] = struct{}{} | ||
|
||
for _, dep := range c.requirements { | ||
// ignore if optional | ||
dc, found := lookupByType(ball, dep) | ||
if !found { | ||
panic(fmt.Sprintf("Dependency %s for %s is missing", dep, c.ID())) | ||
} | ||
_, optional := findTag[Optional](dc) | ||
if optional { | ||
continue | ||
} | ||
collectDependencies(ball, mustLookupByType(ball, dep), result) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// Copyright (C) 2024 Storj Labs, Inc. | ||
// See LICENSE for copying information. | ||
|
||
package mud | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"go.uber.org/zap" | ||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
// RunWithDependencies will init and run all components which are matched by the selector. | ||
func RunWithDependencies(ctx context.Context, ball *Ball, selector ComponentSelector) error { | ||
log := ball.getLogger() | ||
return runComponents(ctx, log, FindSelectedWithDependencies(ball, selector)) | ||
} | ||
|
||
// Run runs the required component and all dependencies in the right order. | ||
func runComponents(ctx context.Context, log *zap.Logger, components []*Component) error { | ||
err := forEachComponent(components, func(component *Component) error { | ||
log.Info("init", zap.String("component", component.Name())) | ||
return component.Init(ctx) | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
g, ctx := errgroup.WithContext(ctx) | ||
err = forEachComponent(components, func(component *Component) error { | ||
log.Info("init", zap.String("starting", component.Name())) | ||
return component.Run(ctx, g) | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
return g.Wait() | ||
} | ||
|
||
// CloseAll calls the close callback stage on all initialized components. | ||
func CloseAll(ball *Ball, timeout time.Duration) error { | ||
ctx := context.Background() | ||
ctx, cancel := context.WithTimeout(ctx, timeout) | ||
defer cancel() | ||
components := ball.registry | ||
reverse(components) | ||
log := ball.getLogger() | ||
return forEachComponent(components, func(component *Component) error { | ||
log.Info("closing", zap.String("component", component.Name())) | ||
if component.instance != nil { | ||
return component.Close(ctx) | ||
} | ||
return nil | ||
}) | ||
} | ||
|
||
// Reverse reverses the elements of the slice in place. | ||
// TODO: use slices.Reverse when minimum golang version is updated. | ||
func reverse[S ~[]E, E any](s S) { | ||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { | ||
s[i], s[j] = s[j], s[i] | ||
} | ||
} |
Oops, something went wrong.