DEV Community 👩‍💻👨‍💻

DEV Community 👩‍💻👨‍💻 is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Arsham Arya
Arsham Arya

Posted on

Trying to shutdown a gin server... (goroutines and channels tips)

Today, I was trying to gracefully shutdown my gin server and start it again through the application itself, without re-running my application.
I learned some precious things that I'd really love to share with you.

So, What's the problem?

Running a server, using gin.Run() or http.ListenAndServe(), will block goroutine, if it's in the main process, so it will block the main goroutine:

func main() {
  ginApp := gin.Default()
  ginApp.Run(":80") // This code is blocking
  // next lines will never run because of blocking code.
  FuncForStoppingGinServer() // This function can't be used.
}
Enter fullscreen mode Exit fullscreen mode

Use another goroutine

We can simply block another goroutine and help main goroutine to remain free.

func main() {
  ginApp := gin.Default()
  go ginApp.Run(":80") // This code is not blocking anymore.
  FuncForStoppingGinServer() // This function will run now.
}
Enter fullscreen mode Exit fullscreen mode

But, what should we do inside FuncForStoppingGinServer()??

Nothing!
The answer is that gin itself doesn't have any function for shutting down its server!
But before getting into that, let's try another wrong way!!

Just kill it!

Are you thinking about stopping your gin server using goroutines and channels? (forget about handling active connections for now)

var ginApp *gin.Engine

func main() {
  // Make an engine
  ginApp = gin.Default()

  // Make a stop channel
  stopChan := make(chan struct{})

  go func() {
    ginApp.Run(":80") // blocking process
    fmt.Println(">> 1 <<")
    for range stopChan {
      return
    }
  }()
  fmt.Println(">> 2 <<")
  // terminate gin server after 3 seconds
  time.Sleep(time.Second * 3)
  stopChan <- struct{}{}
  fmt.Println(">> 3 <<")
}
Enter fullscreen mode Exit fullscreen mode

This sounds to be a good approach, but what do you think is wrong here?

Which numbers will be printed? (1, 2, 3)?

As I mentioned before, ginApp.Run(":80") is a blocking process and it blocks its goroutine, so we will send to channel stopChan,
BUT,
we will never be able to read from the stopChan using our for range loop, because that came after a blocking process and ">> 1 <<" will not be printed.

And as our channel is not buffered, sender that is main goroutine also will be blocked until somebody read from that!, exactly before printing ">> 3 <<" and it will not be printed too.

">> 2 <<" will be printed anyway 😄

Additional fun fact: If we make our channel buffered (size 1 is enough for this case), main goroutine will send to it and go ahead, then it will complete its work and close, whenever main goroutine job is done, the whole program exists! and any other goroutine will die with it, just make this change and see the result:

stopChan := make(chan struct{}, 1)
Enter fullscreen mode Exit fullscreen mode

I also should mention that chan struct{} as the channel type and empty struct struct{}{} will consume no memory at all, and it's known as signal only channel.

I finally realized there is no way to solve this problem this way:
Is there a way to stop a long blocking function?

You can also learn more about stopping goroutines in this Question: How to stop a goroutine
And I'll cover more examples later. (you can follow me to get noticed)

Back into the problem!

So using Goroutines didn't solved our problem,
How about trying http.Server.Shutdown() method?

var ginApp *gin.Engine

func main() {
  // Make an engine
  ginApp = gin.Default()

  // Make a http server
  httpServer := &http.Server{
    Addr:    ":80",
    Handler: ginApp,
  }

  // Launch http server in a separate goroutine
  go httpServer.ListenAndServe()

  // Stop the server
  time.Sleep(time.Second * 5)
  fmt.Println("We're going to stop gin server")
  httpServer.Shutdown(context.Background())
}
Enter fullscreen mode Exit fullscreen mode

It may be helpful to read this question: Graceful stop of gin server

It works, but what is still wrong?

Now, we are shutting down our server inside of our program, but how can we start it again?
Maybe by running ListenAndServe() again?

As I asked in this question: How to stop and start gin server multiple times in one run
I'm trying to implement something like this semi-code:

srv := NewServer()
srv.Start()
srv.Stop()
srv.Start()
srv.Stop()
srv.Start()
Enter fullscreen mode Exit fullscreen mode

Let's do it by a little refactor.

type Server struct {
  httpServer *http.Server
}

func main() {
  srv := NewHTTPServer()
  srv.Start()
  time.Sleep(time.Second * 2)
  srv.Stop()
  time.Sleep(time.Second * 2)
  srv.Start()

  select {} // Simulate other processes
}

func NewHTTPServer() *Server {
  return &Server{
    httpServer: &http.Server{
      Addr:    ":80",
      Handler: gin.Default(),
    },
  }
}

func (s *Server) Start() {
  go func() {
    if err := s.httpServer.ListenAndServe(); err != nil {
      fmt.Println(">>>", err)
    }
  }()
}

func (s *Server) Stop() {
  s.httpServer.Shutdown(context.Background())
}
Enter fullscreen mode Exit fullscreen mode

Run this code and see what happens!
Output:

...Gin logs...
>>> http: Server closed
>>> http: Server closed
Enter fullscreen mode Exit fullscreen mode

What? we called srv.Stop() just once, but our server closed twice!
Why?
According to .Shutdown() functions docs:

Once Shutdown has been called on a server, it may not be reused; future calls to methods such as Serve will return ErrServerClosed.

So, it's officially not working!

But there is still one little change, we can make the server struct again from scratch, every time we start it.

Add a serve function to the last code and change main function, other codes are the same.

func main() {
  srv := serve()
  time.Sleep(time.Second * 2)
  srv.Stop()
  time.Sleep(time.Second * 2)
  srv = serve()
  srv.Start()

  select {} // Simulate other processes
}

func serve() *Server {
  srv := NewHTTPServer()
  // Register handlers or other stuff
  srv.Start()
  return srv
}
Enter fullscreen mode Exit fullscreen mode

This is something that we want, but you can make the code cleaner.

Other solutions

In my case, I was able to use Fiber instead, it fits way much better in my projects, but this may not be the solution you are looking for,
Also you can have a look at this page: Graceful restart or stop

Final quote

This article had a different feeling for myself, it was a up and down path with some tips and some not working approaches, I hope you feel the same and I'll be happy hear your thoughts.

Top comments (0)

🌚 Life is too short to browse without dark mode