Skip to content

Commit

Permalink
private/mud: service lifecycle utilities
Browse files Browse the repository at this point in the history
Change-Id: I3a05a4f84f414a5fed400f6e2ec5166551bb0fb4
  • Loading branch information
elek authored and Storj Robot committed Mar 14, 2024
1 parent ed74f4b commit 42d7317
Show file tree
Hide file tree
Showing 8 changed files with 1,074 additions and 0 deletions.
123 changes: 123 additions & 0 deletions private/mud/README.md
@@ -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.
142 changes: 142 additions & 0 deletions private/mud/component.go
@@ -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
}
61 changes: 61 additions & 0 deletions private/mud/dependency.go
@@ -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)
}
}
63 changes: 63 additions & 0 deletions private/mud/lifecycle.go
@@ -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]
}
}

0 comments on commit 42d7317

Please sign in to comment.