Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Introducing MongoDB 8.0, the fastest MongoDB ever!
MongoDB Developer
Go
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Gochevron-right

Concurrency and Gracefully Closing the MDB Client

Jorge D. Ortiz-Fuentes5 min read • Published Sep 04, 2024 • Updated Sep 04, 2024
Go
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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.

Use custom multiplexer

  1. 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).
    1router := http.newservemux()
    2router.handlefunc("get /", func(w http.responsewriter, r *http.request) {
    3 w.write([]byte("HTTP caracola"))
    4})
    5router.handlefunc("post /notes", createNote)
  2. 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.
    1server := http.Server{
    2 Addr: serverAddr,
    3 Handler: router,
    4}
  3. 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.
    1log.Fatal(server.ListenAndServe())
  4. If you compile and run this version, it should behave exactly the same as before.
  5. The ListenAndServe() function returns a specific error when the server is closed with a Shutdown(). Let's handle it separately.
    1if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
    2 log.Fatalf("HTTP server error %v\n", err)
    3}

Use shutdown function on signal interrupt

Stop signal
  1. The Server type has other methods we can use. Among others, we can define the function that will be executed after Shutdown() has been invoked. This must be done before ListenAndServe() is invoked.
    1server.RegisterOnShutdown(func() {
    2 fmt.Println("Signal shutdown")
    3})
  2. 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.
    1func() {
    2}
  3. 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.
    1sigint := make(chan os.Signal, 1)
    2signal.Notify(sigint, os.Interrupt)
  4. Reading from this channel will wait until an interrupt signal (Control+C) is received.
    1<-sigint
  5. 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.
    1if err := server.Shutdown(context.Background()); err != nil {
    2 log.Fatalf("Server shutdown error: %v", err)
    3}
  6. 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.
    1go func() {
    2 // ...
    3}()

Wait for shutdown to finish

  1. 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 with RegisterOnShutdown() on another goroutine. And depending on the order of execution and how long the registered function is, it might exit main 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.
    1done := make(chan struct{})
  2. 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
  3. 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.
    1defer func(){
    2 done<-struct{}{}
    3}()
  4. Let's add some delay to the function to verify that this is doing its job.
    1time.Sleep(5 * time.Second)
  5. This should solve the situation. Compile and test.

Avoid blocking when not doing early exit

  1. 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 uses os.Exit(), while log.Panic() prints a message and triggers a panic, which causes deferred functions to run.

Conclusion

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.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Go to MongoDB Using Kafka Connectors - Ultimate Agent Guide


Sep 17, 2024 | 7 min read
Code Example

Cinema: Example Go Microservices Application


Sep 11, 2024 | 0 min read
Quickstart

Reacting to Database Changes with MongoDB Change Streams and Go


Feb 03, 2023 | 5 min read
Tutorial

HTTP Servers Persisting Data in MongoDB


Sep 04, 2024 | 5 min read
Table of Contents