Concurrency and Gracefully Closing the MDB Client
Rate this tutorial
In the previous article and the corresponding video, we learned to persist the data that was exchanged with our HTTP server using MongoDB. We used the MongoDB driver for Go to access a free MongoDB Atlas cluster and use instances of our data directly with it.
In this article, we are going to focus on a more advanced topic that often gets ignored: how to properly shut down our server. This can be used with the
WaitGroups
provided by the sync
package, but I decided to do it using goroutines and channels for the sake of getting to cover them in a more realistic but understandable use case.In the latest version of the code of this program, we had set a way to properly close the connection to the database. However, we had no way of gracefully stopping the web server. Using Control+C closed the server immediately and that code was never executed.
- Before we are able to customize the way our HTTP server shuts down, we need to organize the way it is built. First, the routes we created are added to the
DefaultServeMux
. We can create our own router instead, and add the routes to it (instead of the old ones).1 router := http.newservemux() 2 router.handlefunc("get /", func(w http.responsewriter, r *http.request) { 3 w.write([]byte("HTTP caracola")) 4 }) 5 router.handlefunc("post /notes", createNote) - The router that we have just created, together with other configuration parameters, can be used to create an
http.Server
. Other parameters can also be set: Read the documentation for this one.1 server := http.Server{ 2 Addr: serverAddr, 3 Handler: router, 4 } - Use this server to listen to connections, instead of the default one. Here, we don't need parameters in the function because they are provided with the
server
instance, and we are invoking one of its methods.1 log.Fatal(server.ListenAndServe()) - If you compile and run this version, it should behave exactly the same as before.
- The
ListenAndServe()
function returns a specific error when the server is closed with aShutdown()
. Let's handle it separately.1 if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 2 log.Fatalf("HTTP server error %v\n", err) 3 }
- The
Server
type has other methods we can use. Among others, we can define the function that will be executed afterShutdown()
has been invoked. This must be done beforeListenAndServe()
is invoked.1 server.RegisterOnShutdown(func() { 2 fmt.Println("Signal shutdown") 3 }) - Then, we define an anonymous function that waits for the interrupt signal and starts the proper shutdown of the server. We start with an empty function.
1 func() { 2 } - Go handles POSIX signals using
signal.Notify()
. This function takes a channel that will be used to notify and the signal that you want to be handled. A channel is like a pipe in Go with an associated type that is defined when the channel is created. Data is sent to a channel using this notation:channel <- data
. And it is read using this other notation:data <- channel
. If you read from a channel that hasn't any data, the current "execution thread" is stopped and waits for data to be available. If you write data to a channel, the current "execution thread" is stopped and waits for the data to be read. Because of this specific behavior, they are commonly used as a synchronization mechanism. Channels can also have a buffer of a fixed size. Writing to a channel doesn't block until the buffer is full. Let's create the channel that communicates signals (os.Signal
) with a buffer of one element and use it with the function to handle the signal.1 sigint := make(chan os.Signal, 1) 2 signal.Notify(sigint, os.Interrupt) - Reading from this channel will wait until an interrupt signal (Control+C) is received.
1 <-sigint - And when that happens, we can initiate the shut down process. If we get an error then, we will log it and
panic
. We could (should?) have a timeout in this context.1 if err := server.Shutdown(context.Background()); err != nil { 2 log.Fatalf("Server shutdown error: %v", err) 3 } - Now that we have defined the anonymous function, putting parentheses at the end, we invoke the function. However, if we just do that, this function will be executed in the current "execution thread" and our program will wait for the signal and shut down the server without even starting it. We need to create another "execution thread". Fortunately, that is trivial in Go: You can create another "execution thread" by using the
go
keyword before executing a function. That is called a goroutine.1 go func() { 2 // ... 3 }()
- If we run this version of the program, it should be working fine. There is one caveat, though. When
server.Shutdown()
is invoked, the server will stop listening and exit. It will also execute the function we have registered withRegisterOnShutdown()
on another goroutine. And depending on the order of execution and how long the registered function is, it might exitmain
before the registered function ends its job. When the program exits from the main function, any other goroutines get canceled. We can use another channel to avoid that from happening. We create this new channel with no data (empty struct), since it is just meant for synchronization.1 done := make(chan struct{}) - We will read from this channel right before exiting the
main
function. If we haven't written to it yet, we will block there.1 <-done - When we start executing the function that will be run on shutdown, we defer writing to this channel, ensuring that it will be the last thing that will be done when the function finishes, unblocking the end of the execution of the program.
1 defer func(){ 2 done<-struct{}{} 3 }() - Let's add some delay to the function to verify that this is doing its job.
1 time.Sleep(5 * time.Second) - This should solve the situation. Compile and test.
- However, if the server fails because of any error, it will stay there, waiting for the
done
channel to be written. One way to solve that is to close the channel because reading from a closed channel doesn't block. The other one is to use the proper log function to trigger a panic when the error is detected.log.Fatal()
prints and usesos.Exit()
, whilelog.Panic()
prints a message and triggers a panic, which causes deferred functions to run.
With this final article, we have covered:
- The configuration possibilities offered by the HTTP server of the standard library.
- The creation of goroutines that allow concurrent execution of code.
- The use of channels for the synchronization of goroutines.
The repository has all the code for this series so you can follow along. The topics covered in it are the foundations that you need to know to produce full-featured REST APIs, back-end servers, or even microservices written in Go. The road is in front of you and we are looking forward to learning what you will create with this knowledge.
Stay curious. Hack your code. See you next time!
Top Comments in Forums
There are no comments on this article yet.