lcfx
is a small helper library for the go.uber.org/fx dependency injection framework. It simplifies the management of long-running, asynchronous tasks (like web servers, message queue consumers, or other background services) within the fx
application lifecycle.
When using fx
, OnStart
hooks are expected to complete quickly. If a hook blocks for too long (by default, 15 seconds), the application will time out during startup and exit with an error. Starting a long-running service like an HTTP server directly in an OnStart
hook will cause this timeout.
The common workaround is to launch the service in a separate goroutine. However, this requires manual wiring for graceful shutdown and a way to signal startup errors back to the main application, which can be complex.
lcfx
provides a custom Lifecycle
wrapper with an AppendAsync
method. This allows you to write your long-running task logic in a simple, synchronous style, while lcfx
handles the complexity of running it in the background and ensuring graceful shutdown.
To use it, you need to:
- Add
lcfx.Module
to yourfx.New()
call. This provides the custom lifecycle to the dependency graph. - Request
lcfx.Lifecycle
in your constructor instead offx.Lifecycle
. - Use the
lc.AppendAsync
method to register your long-running service.
Here is a complete example:
package main
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/lftk/lcfx" // Import lcfx
"go.uber.org/fx"
)
func main() {
app := fx.New(
// 1. Add the lcfx.Module.
lcfx.Module,
fx.Provide(NewMux, NewHandler),
fx.Invoke(Register),
// Disable fx logging for clarity in this example.
fx.NopLogger,
)
app.Run()
}
// 2. Request lcfx.Lifecycle in the constructor.
func NewMux(lc lcfx.Lifecycle) *http.ServeMux {
mux := http.NewServeMux()
server := &http.Server{
Addr: "127.0.0.1:8080",
Handler: mux,
}
// 3. Use lc.AppendAsync for the long-running task.
lc.AppendAsync(fx.Hook{
OnStart: func(context.Context) error {
fmt.Println("Starting HTTP server at", server.Addr)
// Write blocking code directly. lcfx handles the goroutine.
// If this returns an error, lcfx will shut down the app.
err := server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
fmt.Println("HTTP server shut down gracefully.")
return nil // Expected error on graceful shutdown.
}
return err
},
OnStop: func(ctx context.Context) error {
fmt.Println("Stopping HTTP server.")
return server.Shutdown(ctx)
},
})
return mux
}
func NewHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Got a request.")
fmt.Fprintln(w, "Hello, world!")
})
}
func Register(mux *http.ServeMux, h http.Handler) {
mux.Handle("/", h)
}
go get github.com/lftk/lcfx