A micro-framework for deterministic and graceful services boot and shutdown.
Check the article on Medium about the framework.
Most golang applications are made of multiple services, where each service is an entity that can be started and stopped on-demand. Most of the services depend on each other, for example it makes little sense to start HTTP server until a DB connection is established. Therefore, all the services need to be started in the right order and then stopped in the reverse order. This allows application to boot and shutdown gracefully and deterministically, properly acquiring and releasing all the resources, locks, etc.
Furthermore, each application has to respect OS signals (e.g. SIGINT, SIGTERM). And such the signals can be caught even during the application boot process and in that case they should trigger the shutdown flow immediately. The shutdown flow is also non-trivial, because for most execution environments an application is given a little window to shutdown gracefully (usually 5 to 15 seconds) before it gets SIGKILL. All the flows are usually controlled by a context
provided by the application, either bound to OS signals, or a timeout.
This architecure pattern became very common for most applications, and this is why go-boot
was built as a standalone micro-framework.
- Installation
go get -u github.com/pinebit/go-boot
- Make your services conforming the
boot.Service
interface
package service1
type Service1 interface {
boot.Service
}
- Instantiate all your services
Recommended to use Dependency Injection frameworks, such as wire, fx or any other, for now we simply create all services one by one.
s1 := service1.NewService1(...)
s2 := service2.NewService2(...)
s3 := service3.NewService3(...)
- Start all services respecting the given
context
// You can choose between boot.Sequentially, boot.Simultaneously or combine them.
services := boot.Sequentially(s1, s2, s3)
// the ctx can stop the boot flow gracefully
if err := services.Start(ctx); err != nil {
// report error and proceed with the shutdown
}
You can combine sequential and parallel boot flows. For example, given services A, B, C and D. Where service A must be started first, then B and C can be started simultaneously and then D can be started only after A, B and C have started:
services := boot.Sequentially(a, boot.Simultaneously(b, c), d)
err := services.Start(ctx)
- Shutdown all services respecting a timeout
// The recommended shutdown timeout is five seconds for most systems.
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
// Stop() will stop all services in the reverse order.
// The shutdown flow would break if the ctx is done.
if err := services.Stop(ctx); err != nil {
// One of the services reported error, or ctx is done.
// Log the error for further investigation.
// You still have a good chance to release critical resources, locks.
}
Almost every application is handling OS signals to trigger a shutdown. The framework provides this convenient wrapper, that allows you to build a complete graceful application:
func main() {
// creating services: s1, s2, s3
services := boot.Sequentially(s1, s2, s3)
app := boot.NewApplicationForService(services, 5 * time.Second)
if err := app.Run(context.Background()); err != nil {
fmt.Println("application error:", err)
}
}
The framework also provides a convenient wrapper for http.Server
that gracefully starts/stops the HTTP server.
Together with Application
you get the complete minimalistic yet graceful HTTP server:
func main() {
server := &http.Server{
Addr: ":8080",
}
serverAsService := boot.NewHttpServer(server)
app := boot.NewApplicationForService(serverAsService, 5*time.Second)
if err := app.Run(context.Background()); err != nil {
fmt.Println("server error:", err)
}
}
MIT. See the LICENSE
file for details.