Skip to content

pseidemann/finish

master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
April 3, 2023 21:49
May 28, 2018 14:45
August 19, 2022 10:57
January 22, 2019 22:19
August 19, 2022 10:38
August 19, 2022 12:02
January 15, 2020 13:06
August 19, 2022 10:38
August 19, 2022 10:38

finish

Go Reference Go Report Card Build Status

A non-intrusive package, adding a graceful shutdown to Go's HTTP server, by utilizing http.Server's built-in Shutdown() method, with zero dependencies.

Quick Start

Assume the following code in a file called simple.go:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/pseidemann/finish"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		fmt.Fprintln(w, "world")
	})

	srv := &http.Server{Addr: "localhost:8080"}

	fin := finish.New()
	fin.Add(srv)

	go func() {
		err := srv.ListenAndServe()
		if err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	fin.Wait()
}

Now execute that file:

$ go run simple.go

Do a HTTP GET request:

$ curl localhost:8080/hello

This will print "world" after 5 seconds.

When the server is terminated with pressing Ctrl+C or kill, while /hello is loading, finish will wait until the request was handled, before the server gets killed.

The output will look like this:

2038/01/19 03:14:08 finish: shutdown signal received
2038/01/19 03:14:08 finish: shutting down server ...
2038/01/19 03:14:11 finish: server closed

Customization

Change Timeout

How to change the default timeout of 10 seconds.

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/pseidemann/finish"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		fmt.Fprintln(w, "world")
	})

	srv := &http.Server{Addr: "localhost:8080"}

	fin := &finish.Finisher{Timeout: 30 * time.Second}
	fin.Add(srv)

	go func() {
		err := srv.ListenAndServe()
		if err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	fin.Wait()
}

In this example the timeout is set to 30 seconds.

Change Logger

How to change the default logger.

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/pseidemann/finish"
	"github.com/sirupsen/logrus"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		fmt.Fprintln(w, "world")
	})

	srv := &http.Server{Addr: "localhost:8080"}

	fin := &finish.Finisher{Log: logrus.StandardLogger()}
	fin.Add(srv)

	go func() {
		err := srv.ListenAndServe()
		if err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	fin.Wait()
}

In this example, logrus is configured for logging.

Change Signals

How to change the default signals (SIGINT, SIGTERM) which will initiate the shutdown.

package main

import (
	"fmt"
	"log"
	"net/http"
	"syscall"
	"time"

	"github.com/pseidemann/finish"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		fmt.Fprintln(w, "world")
	})

	srv := &http.Server{Addr: "localhost:8080"}

	fin := &finish.Finisher{Signals: append(finish.DefaultSignals, syscall.SIGHUP)}
	fin.Add(srv)

	go func() {
		err := srv.ListenAndServe()
		if err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	fin.Wait()
}

In this example, finish will not only catch the default signals SIGINT and SIGTERM but also the signal SIGHUP.

Full Example

This example uses a custom router httprouter, a different timeout, a custom logger logrus, custom signals, options for Add() and multiple servers.

package main

import (
	"fmt"
	"log"
	"net/http"
	"syscall"
	"time"

	"github.com/julienschmidt/httprouter"
	"github.com/pseidemann/finish"
	"github.com/sirupsen/logrus"
)

func main() {
	routerPub := httprouter.New()
	routerPub.HandlerFunc("GET", "/hello", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		fmt.Fprintln(w, "world")
	})

	routerInt := httprouter.New()
	routerInt.HandlerFunc("GET", "/status", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "ok")
	})

	srvPub := &http.Server{Addr: "localhost:8080", Handler: routerPub}
	srvInt := &http.Server{Addr: "localhost:3000", Handler: routerInt}

	fin := &finish.Finisher{
		Timeout: 30 * time.Second,
		Log:     logrus.StandardLogger(),
		Signals: append(finish.DefaultSignals, syscall.SIGHUP),
	}
	fin.Add(srvPub, finish.WithName("public server"))
	fin.Add(srvInt, finish.WithName("internal server"), finish.WithTimeout(5*time.Second))

	go func() {
		logrus.Infof("starting public server at %s", srvPub.Addr)
		err := srvPub.ListenAndServe()
		if err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	go func() {
		logrus.Infof("starting internal server at %s", srvInt.Addr)
		err := srvInt.ListenAndServe()
		if err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	fin.Wait()
}