-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Hugo Sjoberg
committed
Sep 21, 2023
1 parent
79a28f9
commit 2ed4e86
Showing
5 changed files
with
169 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 >}} |
This file was deleted.
Oops, something went wrong.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters