Awesome Go

yama

CategoryMiscellaneous
SubcategoryDependency Injection
Stars0

Compile-time dependency injection and lifecycle framework that generates start, quiesce, and stop code for Google Wire graphs

About yama

yama

A compile-time dependency injection and lifecycle framework for Go. Google Wire constructs the dependency graph. Yama generates the code that starts, quiesces, and stops that graph in dependency order, rather than building a runtime engine that interprets one.

Build Status Go Version Documentation Go Report Card OpenSSF Scorecard Coverage Status License

Image of Yama

Status

This is a green-field rewrite; it shares only a name and a repository with the signal-watcher released as v0.1.0 and v0.2.0. The generator and runtime are complete. The project is in alpha: v0.3.0 was its first alpha release, and the public API can still change before v1.0.

Setup

Generation is one command. Yama runs Google Wire for you, reads the injector Wire produces, and writes lifecycle_gen.go.

Pin both tools in the application's go.mod. Yama invokes Wire as go tool wire from the target package's own module, so that module supplies it:

tool (
	github.com/google/wire/cmd/wire
	l7e.io/yama/cmd/yama
)

Declaring a lifecycle

Declare the graph with ordinary Wire providers. Then add a lifecycle stub file, behind the yamainject build tag, naming each constructor the application calls and the providers its graph is built from:

//go:build yamainject

package hello

// NewLifecycle orchestrates the graph GraphSet builds, reporting to w.
func NewLifecycle(w io.Writer, opts ...yama.Option) (*Server, yama.Lifecycle, error) {
	panic(wire.Build(GraphSet))
}

Add the generate directive to a committed file that no build tag excludes; go generate does not read a file a build tag hides, so it cannot live in the stub file:

//go:generate go tool yama

Generating

Then generate:

go generate ./...

Yama's flags and package-pattern argument are wire gen's own, so a directive that already names Wire's command carries over by changing the command it names.

Running

The application calls the generated constructor and runs the lifecycle it returns:

app, lc, err := hello.NewLifecycle(os.Stdout)
if err != nil {
	log.Fatal(err)
}

if err := yama.RunUntilSignal(context.Background(), lc); err != nil {
	log.Fatal(err)
}

RunUntilSignal gives its context to Start and to Stop without a change. Every component and every interceptor therefore receives it. Deployment facts on that context, such as a node identifier, reach all of them. A cancellation and a deadline reach them too. A cancellation also stops the application in the same way that a signal does.

Boundary components

Some components belong at the edges of the graph's order rather than inside it. Two options on the generated constructor register them:

app, lc, err := hello.NewLifecycle(os.Stdout,
	yama.WithBeginComponents(telemetry),
	yama.WithEndComponents(readiness),
)

A begin component starts before every graph component, and it quiesces and stops after every graph component. Base services such as telemetry belong here. They outlive everything that uses them.

An end component starts after every graph component, and it quiesces and stops before every graph component. A readiness flip belongs here. It turns on only when the whole application is up, and it turns off first.

A boundary component participates through the capabilities it implements, exactly as a graph component does. Both options are variadic and accumulate across calls.

Files in the package directory

lifecycle_gen.go is the only file generation commits. A run also writes two transient files into the package directory and removes both before it returns:

FileOwnerCommitted
lifecycle_gen.goYamayes
wire_gen.goWireno, transient
yama_wireinject.goYamano, transient

A run does not overwrite a wire_gen.go that it did not create. It moves that file to wire_gen.go.bak for the run, and puts it back at the end. An application that commits its own Wire output therefore keeps it. A run moves a committed lifecycle_gen.go to lifecycle_gen.go.bak the same way.

Yama owns the name yama_wireinject.go. A run writes over a file already at that name. Do not keep a file of your own there.

Do not start two runs over one package directory at the same time. Yama does not lock the directory. The second run's cleanup can delete a wire_gen.go that the first run put back.

Recovering from an interrupted run

A run that stops before it completes does not reach its cleanup. It can leave these files behind:

  • wire_gen.go.bak — your wire_gen.go, if you committed one.
  • lifecycle_gen.go.bak — your committed lifecycle_gen.go.
  • wire_gen.go — Wire's output from that run, not yours.
  • yama_wireinject.go — Yama's derived injectors.

Generate again. The next run repairs the directory before it reads anything:

go generate ./...

It puts both .bak files back under their original names first, and discards the output the interrupted run left. You lose nothing. Yama creates a .bak file only to hold a file of yours.

Example

examples/hello is a working application built this way. It is its own Go module, so it reaches Yama the way an application does.

Guide

docs/guide.md is the behavior reference: the lifecycle model, ordering, errors, interceptors, the context, and the command.

Design

For the design, see:

Frequently Asked Questions

What is yama?

yama is a Miscellaneous library for the Go programming language. Compile-time dependency injection and lifecycle framework that generates start, quiesce, and stop code for Google Wire graphs

How do I install yama?

Install yama with the Go module system using `go get livetribe/yama`. Check the repository for the current installation instructions.

What category does yama belong to?

yama is listed under Miscellaneous, specifically Dependency Injection.

← Back to Dependency Injection