Preface
When starting a Golang http service in a conventional way, if the service is terminated or interrupted unexpectedly, it is not waiting for the service to process the existing request connection and return normally and does not perform some necessary processing before the service is stopped, which will cause the service to be hard termination. This method is not very elegant.
See the following code. The http service request path is the root path. If you request this path, it will return hello after 2s.
var addr = ("server addr", ":8080", "server address") func main() { ("/", func(w , r *) { (2 * ) (w, "hello") }) (*addr, nil) }
If after the service is started, request http://localhost:8080/, and then use Ctrl+C to interrupt the service immediately, the service will exit immediately (exit status 2), the request does not return normally (ERR_CONNECTION_REFUSED), and the connection will be broken immediately.
Next, we introduce the use of Shutdown method combined with elegant termination of services.
1 Shutdown method
The Golang structure has a method to terminate the service Shutdown, whose go doc is as follows.
func (srv *Server) Shutdown(ctx ) error
Shutdown gracefully shuts down the server without interrupting any active
connections. Shutdown works by first closing all open listeners, then
closing all idle connections, and then waiting indefinitely for connections
to return to idle and then shut down. If the provided context expires before
the shutdown is complete, Shutdown returns the context's error, otherwise it
returns any error returned from closing the Server's underlying Listener(s).When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
immediately return ErrServerClosed. Make sure the program doesn't exit and
waits instead for Shutdown to return.Shutdown does not attempt to close nor wait for hijacked connections such as
WebSockets. The caller of Shutdown should separately notify such long-lived
connections of shutdown and wait for them to close, if desired. See
RegisterOnShutdown for a way to register shutdown notification functions.Once Shutdown has been called on a server, it may not be reused; future
calls to methods such as Serve will return ErrServerClosed.
From the documentation, we can see:
Use Shutdown to terminate the service gracefully without interrupting active connections.
The working process is: first, turn off all open listeners, then close all idle connections, and finally wait until the active connections are idle before termination of the service.
If the incoming context has timed out before the service has completed termination, the Shutdown method returns an error in context, otherwise any errors caused by shutting down the service listener are returned.
When the Shutdown method is called, the Serve, ListenAndServe and ListenAndServeTLS methods will immediately return an ErrServerClosed error. Please make sure that Shutdown does not return and do not exit the program.
For long connections such as WebSocket, Shutdown will not try to close or wait for those connections. If necessary, the caller needs to separate additional processing (such as notifying long connections or waiting for them to close, registering the termination notification function with RegisterOnShutdown).
Once Shutdown is called on the server, it cannot be used again (ErrServerClosed error will be reported).
With the Shutdown method, we know that before the service terminates, we can call this method to wait for the active connection to return normally and then close gracefully.
But at some point after the service is started, how does the program know that the service has been interrupted? How to notify the program when the service is interrupted and then call Shutdown for processing? Next, let’s take a look at the function of the system signal notification function.
2 functions
The Notify function of the signal package provides the ability to signal the system, and its go doc is as follows.
func Notify(c chan<- , sig ...)
Notify causes package signal to relay incoming signals to c. If no signals
are provided, all incoming signals will be relayed to c. Otherwise, just the
provided signals will.Package signal will not block sending to c: the caller must ensure that c
has sufficient buffer space to keep up with the expected signal rate. For a
channel used for notification of just one signal value, a buffer of size 1
is sufficient.It is allowed to call Notify multiple times with the same channel: each call
expands the set of signals sent to that channel. The only way to remove
signals from the set is to call Stop.It is allowed to call Notify multiple times with different channels and the
same signals: each channel receives copies of incoming signals
independently.
From the documentation, we can see:
Parameter c is the caller's signal reception channel, and Notify can transfer the incoming signal to c. The sig parameter is the type of signal to be forwarded. If not specified, all incoming signals will be transferred to c.
The signal will not be blocked to c: The caller needs to ensure that c has enough buffer space to cope with the high-frequency transmission of the specified signal. For channels that are used to notify only one signal value, the buffer size is 1.
Notify can be called multiple times for the same channel: each call extends the set of signals sent to the channel. Stop can only be called to remove signals from the signal set.
Allow different channels to call Notify multiple times using the same signal parameters: each channel receives a copy of the incoming signal independently.
In summary, with , pass in a chan and specify the interrupt parameters, so that when the system is interrupted, a signal can be received.
See the following code. When using Ctrl+C, c will receive an interrupt signal, and the program will exit after printing the "program interrupted" statement.
func main() { c := make(chan ) (c, ) <-c ("program interrupted") }
$ go run
Ctrl+C
2019/06/11 17:59:11 program interrupted
exit status 1
3 Server elegant termination
Next, we use the Shutdown method combined as above to achieve elegant termination of the service.
As shown in the following code, Handler is the same as the processing logic at the beginning of the article, and it will return to hello after 2s.
Create an instance, specifying the port and Handler.
Declare a processed chan, which is used to ensure that the service is terminated elegantly before exiting the main goroutine.
A new goroutine is enabled, which will listen for the signal. Once the service is interrupted, the service's Shutdown method is called to ensure the normal return of the active connection (the Context timeout used by this code is 3s, which is greater than the processing time of the service handler, so it will not time out).
After the processing is completed, close the processed channel and finally the main goroutine exits.
The code is also hosted on GitHub, please follow (/olzhy/go-excercises).
var addr = ("server addr", ":8080", "server address") func main() { // handler handler := (func(w , r *) { (2 * ) (w, "hello") }) // server srv := { Addr: *addr, Handler: handler, } // make sure idle connections returned processed := make(chan struct{}) go func() { c := make(chan , 1) (c, ) <-c ctx, cancel := ((), 3*) defer cancel() if err := (ctx); nil != err { ("server shutdown failed, err: %v\n", err) } ("server gracefully shutdown") close(processed) }() // serve err := () if != err { ("server not gracefully shutdown, err :%v\n", err) } // waiting for goroutine above processed <-processed }
Summarize
This is the end of this article about how Golang terminates a service elegantly. For more relevant content on Golang terminates a service, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!