Skip to content

Commit

Permalink
Add blog graceful-shutdown
Browse files Browse the repository at this point in the history
  • Loading branch information
Hugo Sjoberg committed Sep 21, 2023
1 parent 79a28f9 commit 2ed4e86
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 84 deletions.
98 changes: 98 additions & 0 deletions content/english/blog/golang/graceful-shutdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: "Graceful shutdown of server in Go"
meta_title: ""
description: "Graceful shutdown of server in Go"
date: 2023-09-18T05:00:00Z
categories: ["golang", "server", "shutdown", "concurrency"]
author: "Hugo Sjoberg"
tags: ["golang", "concurrency", "shutdown", "server"]
draft: false
---

# Graceful shutdown

Graceful shutdown refers to shutting down an application or service in a way that allows it to finish any ongoing tasks or transactions, clean up resources, and exit in an orderly and controlled manner. This is important to ensure that the application does not leave any unfinished work, corrupt data, or cause disruptions when it is terminated.

For example, we might be running a service in Kubernetes, and we are experiencing lower traffic so we would naturally scale down the number of pods. The load balancer will not redirect traffic to pods that are in a terminated state.

Now our pods cannot just be terminated, since they might handle a long-running connection with a client(slow database query for example). Let's see how we can gracefully terminate a server in golang.

Golang std-lib provides this neat little package called [errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup)

> Package errgroup provides synchronization, error propagation, and Context cancelation for groups of goroutines working on subtasks of a common task.
The context derived from errgroup got this nice property

> The derived Context is canceled the first time a function passed to Go returns a non-nil error or the first time Wait returns, whichever occurs first.
So whenever one of the goroutines encountered an error this context will cancel.

Now that sounds pretty neat, let's take errgroup out on a little spin and create a server that gracefully exists whenever a termination signal(CTRL+C) comes in.

```golang
package main

import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"

"golang.org/x/sync/errgroup"
)

func main() {
ctxWithCancel, cancel := context.WithCancel(context.Background())
defer cancel()
g, ctx := errgroup.WithContext(ctxWithCancel)

srv := &http.Server{Addr: fmt.Sprintf("0.0.0.0:%d", 8080)}

g.Go(func() error {
fmt.Println("starting server")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
})

g.Go(func() error {
<-ctx.Done()
shutDownCTX, cancel := context.WithTimeout(ctx, 1*time.Second)
if err := srv.Shutdown(context.Background()); err != nil {
fmt.Println("encountered errors when shutting down")
}
fmt.Println("server shut down")
return nil
})

g.Go(func() error {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
<-signals
fmt.Println("terminating...")
cancel()
return nil
})

err := g.Wait()
if err != nil {
fmt.Println(err)
}
}
```

Let's run this to see what happens:
```bash
> go main.go
starting server
terminating... <- CTRL+C to terminate
server shut down
```

Using errgroup we create a couple of goroutines:
- goroutine for running the server
- goroutine for shutting down the server, it's waiting for the context `ctx` to be done and then shuts down the server
- goroutine to check for any kind of termination signal from the OS
70 changes: 70 additions & 0 deletions content/english/blog/golang/singleflight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Imagine you are running a service that calls a slow operation, let's say it makes an expensive query to a database. Now this particular service is a popular one, before the first query even returned a result 10 more identical requests were made.

Wouldn't it be nice if we didn't have to call the database 10 times but instead return the data we get back from the first request to all other requests?

Well, this is usually where caching comes in, but let's be a little bit more experimental and give the package `singleflight` a swing.

This is how godoc describes singleflight:

> Package singleflight provides a duplicate function call suppression mechanism.
Alright sounds promising, the `singleflight` package in Go is a utility for managing duplicate function calls concurrently and efficiently. It helps ensure that a given function is executed only once even if multiple goroutines attempt to invoke it simultaneously.

```golang
package main

import (
"fmt"
"net/http"
"time"

"golang.org/x/sync/singleflight"
)

var (
counter = 0
group singleflight.Group
)

type Response struct {
Message string
}

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
result, err, _ := group.Do("unique-key", func() (interface{}, error) {
val := incCounter()
return &Response{Message: fmt.Sprintf("called %d times", val)}, nil
})
if err != nil {
fmt.Fprintf(w, "error %s", err.Error())
}
fmt.Fprintf(w, result.(*Response).Message)
})

http.ListenAndServe(":8080", nil)
}

func incCounter() int {
time.Sleep(10 * time.Second)
counter = counter + 1
return counter
}
```

Let's run this to see what happens:
```bash
> go run server.go
> curl localhost:8080/ <- New terminal window
> called 1 times
> curl localhost:8080/ <- New terminal window
> called 1 times
```

As we can see by this example the slow function `incCounter` is only invoked one time :rocket:

`group.Do` accepts a string that serves as an identifier or similar for a call, the third returned value indicated is the returned value shared.

{{< notice "tip" >}}
The shared value is important if the returned value is a pointer then we might have to handle that to ensure we don't run into nasty race conditions
{{< /notice >}}
84 changes: 0 additions & 84 deletions content/english/blog/post-2.md

This file was deleted.

File renamed without changes.
1 change: 1 addition & 0 deletions hugo_stats.json
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@
"emphasis",
"gallery",
"gathering-of-personal-information",
"graceful-shutdown",
"heading-1",
"heading-2",
"heading-3",
Expand Down

0 comments on commit 2ed4e86

Please sign in to comment.