Skip to content

Commit

Permalink
docs: runtime docs (#22816)
Browse files Browse the repository at this point in the history
(cherry picked from commit aa8266e)

# Conflicts:
#	depinject/README.md
#	runtime/v2/app.go
#	simapp/app.go
#	simapp/simd/cmd/root.go
  • Loading branch information
julienrbrt authored and mergify[bot] committed Dec 11, 2024
1 parent 721e838 commit ccfe50e
Show file tree
Hide file tree
Showing 8 changed files with 773 additions and 561 deletions.
205 changes: 205 additions & 0 deletions depinject/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
sidebar_position: 1
---

# Depinject

> **DISCLAIMER**: This is a **beta** package. The SDK team is actively working on this feature and we are looking for feedback from the community. Please try it out and let us know what you think.
## Overview

`depinject` is a dependency injection (DI) framework for the Cosmos SDK, designed to streamline the process of building and configuring blockchain applications. It works in conjunction with the `core/appconfig` module to replace the majority of boilerplate code in `app.go` with a configuration file in Go, YAML, or JSON format.

`depinject` is particularly useful for developing blockchain applications:

* With multiple interdependent components, modules, or services. Helping manage their dependencies effectively.
* That require decoupling of these components, making it easier to test, modify, or replace individual parts without affecting the entire system.
* That are wanting to simplify the setup and initialisation of modules and their dependencies by reducing boilerplate code and automating dependency management.

By using `depinject`, developers can achieve:

* Cleaner and more organised code.
* Improved modularity and maintainability.
* A more maintainable and modular structure for their blockchain applications, ultimately enhancing development velocity and code quality.

* [Go Doc](https://pkg.go.dev/cosmossdk.io/depinject)

## Usage

The `depinject` framework, based on dependency injection concepts, streamlines the management of dependencies within your blockchain application using its Configuration API. This API offers a set of functions and methods to create easy to use configurations, making it simple to define, modify, and access dependencies and their relationships.

A core component of the [Configuration API](https://pkg.go.dev/github.com/cosmos/cosmos-sdk/depinject#Config) is the `Provide` function, which allows you to register provider functions that supply dependencies. Inspired by constructor injection, these provider functions form the basis of the dependency tree, enabling the management and resolution of dependencies in a structured and maintainable manner. Additionally, `depinject` supports interface types as inputs to provider functions, offering flexibility and decoupling between components, similar to interface injection concepts.

By leveraging `depinject` and its Configuration API, you can efficiently handle dependencies in your blockchain application, ensuring a clean, modular, and well-organised codebase.

Example:

```go
package main

import (
"fmt"

"cosmossdk.io/depinject"
)

type AnotherInt int

func GetInt() int { return 1 }
func GetAnotherInt() AnotherInt { return 2 }

func main() {
var (
x int
y AnotherInt
)

fmt.Printf("Before (%v, %v)\n", x, y)
depinject.Inject(
depinject.Provide(
GetInt,
GetAnotherInt,
),
&x,
&y,
)
fmt.Printf("After (%v, %v)\n", x, y)
}
```

In this example, `depinject.Provide` registers two provider functions that return `int` and `AnotherInt` values. The `depinject.Inject` function is then used to inject these values into the variables `x` and `y`.

Provider functions serve as the basis for the dependency tree. They are analysed to identify their inputs as dependencies and their outputs as dependents. These dependents can either be used by another provider function or be stored outside the DI container (e.g., `&x` and `&y` in the example above). Provider functions must be exported.

### Interface type resolution

`depinject` supports the use of interface types as inputs to provider functions, which helps decouple dependencies between modules. This approach is particularly useful for managing complex systems with multiple modules, such as the Cosmos SDK, where dependencies need to be flexible and maintainable.

For example, `x/bank` expects an [AccountKeeper](https://pkg.go.dev/cosmossdk.io/x/bank/types#AccountKeeper) interface as [input to ProvideModule](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/x/bank/module.go#L208-L260). `SimApp` uses the implementation in `x/auth`, but the modular design allows for easy changes to the implementation if needed.

Consider the following example:

```go
package duck

type Duck interface {
quack()
}

type AlsoDuck interface {
quack()
}

type Mallard struct{}
type Canvasback struct{}

func (duck Mallard) quack() {}
func (duck Canvasback) quack() {}

type Pond struct {
Duck AlsoDuck
}
```

And the following provider functions:

```go
func GetMallard() duck.Mallard {
return Mallard{}
}

func GetPond(duck Duck) Pond {
return Pond{Duck: duck}
}

func GetCanvasback() Canvasback {
return Canvasback{}
}
```

In this example, there's a `Pond` struct that has a `Duck` field of type `AlsoDuck`. The `depinject` framework can automatically resolve the appropriate implementation when there's only one available, as shown below:

```go
var pond Pond

depinject.Inject(
depinject.Provide(
GetMallard,
GetPond,
),
&pond)
```

This code snippet results in the `Duck` field of `Pond` being implicitly bound to the `Mallard` implementation because it's the only implementation of the `Duck` interface in the container.

However, if there are multiple implementations of the `Duck` interface, as in the following example, you'll encounter an error:

```go
var pond Pond

depinject.Inject(
depinject.Provide(
GetMallard,
GetCanvasback,
GetPond,
),
&pond)
```

A specific binding preference for `Duck` is required.

#### `BindInterface` API

In the above situation registering a binding for a given interface binding may look like:

```go
depinject.Inject(
depinject.Configs(
depinject.BindInterface(
"duck/duck.Duck",
"duck/duck.Mallard",
),
depinject.Provide(
GetMallard,
GetCanvasback,
GetPond,
),
),
&pond)
```

Now `depinject` has enough information to provide `Mallard` as an input to `APond`.

### Full example in real app

:::warning
When using `depinject.Inject`, the injected types must be pointers.
:::

```go reference
https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-beta.2/simapp/app_di.go#L187-L206
```

## Debugging

Issues with resolving dependencies in the container can be done with logs and [Graphviz](https://graphviz.org) renderings of the container tree.
By default, whenever there is an error, logs will be printed to stderr and a rendering of the dependency graph in Graphviz DOT format will be saved to `debug_container.dot`.

Here is an example Graphviz rendering of a successful build of a dependency graph:
![Graphviz Example](https://raw.githubusercontent.com/cosmos/cosmos-sdk/ff39d243d421442b400befcd959ec3ccd2525154/depinject/testdata/example.svg)

Rectangles represent functions, ovals represent types, rounded rectangles represent modules and the single hexagon
represents the function which called `Build`. Black-colored shapes mark functions and types that were called/resolved
without an error. Gray-colored nodes mark functions and types that could have been called/resolved in the container but
were left unused.

Here is an example Graphviz rendering of a dependency graph build which failed:
![Graphviz Error Example](https://raw.githubusercontent.com/cosmos/cosmos-sdk/ff39d243d421442b400befcd959ec3ccd2525154/depinject/testdata/example_error.svg)

Graphviz DOT files can be converted into SVG's for viewing in a web browser using the `dot` command-line tool, ex:

```txt
dot -Tsvg debug_container.dot > debug_container.svg
```

Many other tools including some IDEs support working with DOT files.
108 changes: 108 additions & 0 deletions runtime/v2/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package runtime

import (
"encoding/json"

runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/registry"
"cosmossdk.io/core/transaction"
"cosmossdk.io/log"
"cosmossdk.io/schema/decoding"
"cosmossdk.io/server/v2/appmanager"
"cosmossdk.io/server/v2/stf"
)

// App is a wrapper around AppManager and ModuleManager that can be used in hybrid
// app.go/app config scenarios or directly as a servertypes.Application instance.
// To get an instance of *App, *AppBuilder must be requested as a dependency
// in a container which declares the runtime module and the AppBuilder.Build()
// method must be called.
//
// App can be used to create a hybrid app.go setup where some configuration is
// done declaratively with an app config and the rest of it is done the old way.
// See simapp/v2/app.go for an example of this setup.
type App[T transaction.Tx] struct {
appmanager.AppManager[T]

// app configuration
logger log.Logger
config *runtimev2.Module

// state
stf *stf.STF[T]
msgRouterBuilder *stf.MsgRouterBuilder
queryRouterBuilder *stf.MsgRouterBuilder
db Store
storeLoader StoreLoader

// modules
interfaceRegistrar registry.InterfaceRegistrar
amino registry.AminoRegistrar
moduleManager *MM[T]
queryHandlers map[string]appmodulev2.Handler // queryHandlers defines the query handlers
}

// Name returns the app name.
func (a *App[T]) Name() string {
return a.config.AppName
}

// Logger returns the app logger.
func (a *App[T]) Logger() log.Logger {
return a.logger
}

// ModuleManager returns the module manager.
func (a *App[T]) ModuleManager() *MM[T] {
return a.moduleManager
}

// DefaultGenesis returns a default genesis from the registered modules.
func (a *App[T]) DefaultGenesis() map[string]json.RawMessage {
return a.moduleManager.DefaultGenesis()
}

// SetStoreLoader sets the store loader.
func (a *App[T]) SetStoreLoader(loader StoreLoader) {
a.storeLoader = loader
}

// LoadLatest loads the latest version.
func (a *App[T]) LoadLatest() error {
return a.storeLoader(a.db)
}

// LoadHeight loads a particular height
func (a *App[T]) LoadHeight(height uint64) error {
return a.db.LoadVersion(height)
}

// LoadLatestHeight loads the latest height.
func (a *App[T]) LoadLatestHeight() (uint64, error) {
return a.db.GetLatestVersion()
}

// QueryHandlers returns the query handlers.
func (a *App[T]) QueryHandlers() map[string]appmodulev2.Handler {
return a.queryHandlers
}

// SchemaDecoderResolver returns the module schema resolver.
func (a *App[T]) SchemaDecoderResolver() decoding.DecoderResolver {
moduleSet := map[string]any{}
for moduleName, module := range a.moduleManager.Modules() {
moduleSet[moduleName] = module
}

for _, overrideKey := range a.config.OverrideStoreKeys {
moduleSet[overrideKey.KvStoreKey] = moduleSet[overrideKey.ModuleName]
}

return decoding.ModuleSetDecoderResolver(moduleSet)
}

// Close is called in start cmd to gracefully cleanup resources.
func (a *App[T]) Close() error {
return nil
}
Loading

0 comments on commit ccfe50e

Please sign in to comment.