1
\$\begingroup\$

Good day, everyone!

I want to learn the proper way to gracefully shutdown a http.Server{}. Some answers I found would not handle ctrl-c signals. Others would handle it, but it would lock on the channel read if the server closed itself.

To handle the above situations, I came up with this template. I'm not very experienced when system signals, http.Server and concurrency are all mixed.

So my questions are:

  • Am I missing something?
  • Could I remove the sync.WaitGroup{}, kill and closed channels? And let the program exits and discard the goroutine?
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"
)

func main() {
    log.Print("main: start")
    srv := http.Server{
        Addr: ":8080",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintln(w, "hello!!!")
        }), 
    }   

    stop := make(chan os.Signal, 1)
    kill := make(chan os.Signal, 1)
    closed := make(chan struct{}, 1)

    signal.Notify(stop, os.Interrupt)
    signal.Notify(kill, os.Kill)

    wg := sync.WaitGroup{}

    wg.Add(1)
    go func() {
        defer wg.Done()
        log.Print("goroutine: start")

        select {
        case <-stop:
            log.Print("goroutine: interrupt signal: server shutdown: start")

            if err := srv.Shutdown(context.TODO()); err != nil {
                log.Print("goroutine: interrupt signal: server shutdown: ", err)
            }   

            log.Print("goroutine: interrupt signal: server shutdown: exit")
        case <-kill:
            log.Print("goroutine: kill signal")
            // do nothing?
        case <-closed:
            log.Print("goroutine: server closed")
            // do nothing
        }   

        log.Print("goroutine: exit")
    }() 

    log.Print("server: listen start")
    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Print("server error: ", err)
    }   
    log.Print("server: listen exit")

    closed <- struct{}{}

    wg.Wait()

    log.Print("main: exit")
}
```
\$\endgroup\$
2
  • \$\begingroup\$ I have written HTTP servers in Python and even with handling Ctrl-C sometimes it just stays on, now you can kill the process but recently for some reason that hasn't been working for me. I am not sure if there is any way to make sure that the HTTP server is shutdown except just changing the port of it. I think maybe using a key like "Q" instead of Ctrl-C would be better but I have yet to test that theory. \$\endgroup\$
    – The_AH
    Oct 27, 2023 at 12:04
  • \$\begingroup\$ Also I have been not using Go but Python on MacOS Catalina (your OS might be different) so maybe what works and doesn't work for me would be different on your OS. \$\endgroup\$
    – The_AH
    Oct 27, 2023 at 12:06

1 Answer 1

0
\$\begingroup\$
signal.Notify(kill, os.Kill)

os.Kill will always terminate the process without giving the process a chance to notify something, so kill will not receive a message. The two signals you probably want are SIGINT and SIGTERM (not SIGKILL). ctrl-c sends SIGINT, so if you just want to trap ctrl-c, you don't need to replace this, otherwise replace the signal with syscall.SIGTERM. If you do the latter, you don't need to use a separate kill channel.

closed := make(chan struct{}, 1)

I think this channel is unnecessary. Closing stop will also exit the goroutine.

Therefore you can simplify to the following:

@@ -20,11 +20,8 @@ func main() {
    }
 
    stop := make(chan os.Signal, 1)
-   kill := make(chan os.Signal, 1)
-   closed := make(chan struct{}, 1)
 
    signal.Notify(stop, os.Interrupt)
-   signal.Notify(kill, os.Kill)
 
    wg := sync.WaitGroup{}
 
@@ -33,23 +30,13 @@ func main() {
        defer wg.Done()
        log.Print("goroutine: start")
 
-       select {
-       case <-stop:
+       <-stop
        log.Print("goroutine: interrupt signal: server shutdown: start")
 
        if err := srv.Shutdown(context.TODO()); err != nil {
            log.Print("goroutine: interrupt signal: server shutdown: ", err)
        }
 
-           log.Print("goroutine: interrupt signal: server shutdown: exit")
-       case <-kill:
-           log.Print("goroutine: kill signal")
-           // do nothing?
-       case <-closed:
-           log.Print("goroutine: server closed")
-           // do nothing
-       }
-
        log.Print("goroutine: exit")
    }()
 
@@ -59,7 +46,7 @@ func main() {
    }
    log.Print("server: listen exit")
 
-   closed <- struct{}{}
+   close(stop)
 
    wg.Wait()
 
  • Could I remove the sync.WaitGroup{}, kill and closed channels? And let the program exits and discard the goroutine?

No, this does not follow the documentation of https://pkg.go.dev/net/http#Server.Shutdown:

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.