Introduction to xrun: A Flexible Package for Managing Component Lifecycles in Go
Hello, Go community! Today, I am excited to announce a Golang package I have been working on named xrun
. This package provides a streamlined way of running multiple components, specifically long-running components like an HTTP server or background worker. In this blog post, I will walk you through the basic functionality of xrun
and how it can help you manage your long-running components more efficiently.
Introducing xrun
As the size and complexity of a Go service increase, managing the lifecycles of its various components becomes a daunting task. This post introduces xrun
, a flexible and versatile Go package that simplifies this process, making it easier to reason about and manage your application’s components.
The package provides a Manager
struct that handles starting and stopping components, coordinating their lifecycles in a way that makes your application more maintainable and less prone to bugs.
The Manager
works by starting each component in its own goroutine and waiting for them to either finish or fail. It also ensures that the stop signals are propagated in reverse order of the components’ starting order, ensuring graceful shutdowns.
xrun
is available at github.com/gojekfarm/xrun, and you can read the full documentation on pkg.go.dev.
Getting Started
Let’s start by importing the xrun
package:
import "github.com/gojekfarm/xrun"
To illustrate how xrun
works, let’s create an instance of an HTTP server:
import "net/http"
server := http.Server{Addr: ":9090"}
Next, create a new manager with xrun.NewManager()
. Then use m.Add()
to add the HTTP server to the manager:
import (
"github.com/gojekfarm/xrun"
"github.com/gojekfarm/xrun/component"
)
m := xrun.NewManager()
_ = m.Add(component.HTTPServer(component.HTTPServerOptions{Server: &server}))
Finally, run the manager using m.Run(ctx)
. If there is an error, the process will exit:
import (
"os"
"os/signal"
)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := m.Run(ctx); err != nil {
os.Exit(1)
}
Features of xrun
Now, let’s dive into some features of xrun
.
Component
The Component
interface is the foundation of xrun
. It allows you to start a component that will run until the context is closed or an error occurs:
type Component interface {
Run(context.Context) error
}
ComponentFunc
ComponentFunc
is a helper that allows you to implement Component
inline. It’s perfect for writing custom components on the fly:
type ComponentFunc func(ctx context.Context) error
Manager
The Manager
type helps you to run multiple components and waits for them to complete. You can add components with the Add()
method and run them with the Run()
method:
type Manager struct {
// ...
}
func (m *Manager) Add(c Component) error {...}
func (m *Manager) Run(ctx context.Context) (err error) {...}
Note: Since
Manager
has aRun(context.Context) error
method, it is aComponent
in itself, and hence, it is possible to nest multipleManager(s)
.
Creating and Using Components
A component in xrun
is anything that implements the Component
interface. This interface only requires a single method: Run(context.Context) error
.
Here’s an example of a basic component, a ScheduledTask
:
type ScheduledTask struct {
Ticker *time.Ticker
Task func()
}
func (s *ScheduledTask) Run(ctx context.Context) error {
for {
select {
case <-s.Ticker.C:
s.Task()
case <-ctx.Done():
return nil
}
}
}
In the main function, you create a manager and add your components to it:
m := xrun.NewManager()
tick := time.NewTicker(1 * time.Second)
task := func() { fmt.Println("Task executed") }
s := &ScheduledTask{Ticker: tick, Task: task}
_ = m.Add(s)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := m.Run(ctx); err != nil {
log.Fatal(err)
}
Combining Components with xrun.All
Components can also be combined together using the xrun.All
function. This function creates a new Component
that starts all of the provided components in the order they were given and stops them in the reverse order.
Here is an example of its usage:
tick1 := time.NewTicker(1 * time.Second)
task1 := func() { fmt.Println("Task1 executed") }
s1 := &ScheduledTask{Ticker: tick1, Task: task1}
tick2 := time.NewTicker(2 * time.Second)
task2 := func() { fmt.Println("Task2 executed") }
s2 := &ScheduledTask{Ticker: tick2, Task: task2}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := xrun.All(xrun.NoTimeout, s1, s2).Run(ctx); err != nil {
log.Fatal(err)
}
Extending xrun: Custom Component Examples
While xrun
provides some built-in components, you can create your own to suit your specific needs.
HTTP Server
The xrun
package includes an HTTP server component out of the box. It is available in the xrun/component
subpackage.
gRPC Server (Experimental)
For more advanced use cases, you can create custom components like a gRPC server. An experimental gRPC server component is available in the component/x/grpc
subpackage.
Here’s an example of how you might create such a component:
package grpc
import (
"context"
"net"
"github.com/gojekfarm/xrun"
"google.golang.org/grpc"
)
// Options holds options for Server
type Options struct {
Server *grpc.Server
NewListener func() (net.Listener, error)
}
// Server is a helper which returns a xrun.ComponentFunc to start a grpc.Server
func Server(opts Options) xrun.ComponentFunc {
srv := opts.Server
nl := opts.NewListener
return func(ctx context.Context) error {
l, err := nl()
if err != nil {
return err
}
errCh := make(chan error, 1)
go func(errCh chan error) {
if err := srv.Serve(l); err != nil && err != grpc.ErrServerStopped {
errCh <- err
}
}(errCh)
select {
case <-ctx.Done():
case err := <-errCh:
return err
}
srv.GracefulStop()
return nil
}
}
func NewListener(address string) func() (net.Listener, error) {
return func() (net.Listener, error) {
return net.Listen("tcp", address)
}
}
In this example, we define a new Options
struct that includes our gRPC server instance and a NewListener
function. This function creates a network listener on the given address.
Next, we define a Server
function that takes an Options
instance and returns a xrun.ComponentFunc
. This ComponentFunc
starts the gRPC server and manages its lifecycle. It starts the server in a goroutine and then enters a select block. If the context is done, the gRPC server is stopped gracefully. If an error occurs while serving, it’s returned.
The NewListener
function is a helper that generates a function for creating a network listener.
Here’s how to use it:
package main
import (
"context"
"net"
"os"
"os/signal"
xgrpc "yourgrpcpackage"
"github.com/gojekfarm/xrun"
"google.golang.org/grpc"
)
func main() {
s := grpc.NewServer()
m := xrun.NewManager()
grpcComponent := xgrpc.Server(xgrpc.Options{
Server: s,
NewListener: xgrpc.NewListener(":8500"),
})
_ = m.Add(grpcComponent)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := m.Run(ctx); err != nil {
os.Exit(1)
}
}
Conclusion
The xrun
package makes it easier to manage component lifecycles in your Go applications. By allowing you to define and control how each component starts and stops, you can make your application more maintainable and robust.
Finally, I’d like to give credits to the authors at the Kubernetes community. The manager source of xrun
has been heavily influenced by the sigs.k8s.io/controller-runtime package.
Thanks for reading, and I hope you find xrun
as useful as I do!
Contributions, questions, and feedback are most welcome on the xrun GitHub repository. Happy coding, Gophers!