Skip to content

Commit 6fc1aa8

Browse files
committed
✨ (go-test): add graceful shutdown sample
0 parents  commit 6fc1aa8

File tree

8 files changed

+341
-0
lines changed

8 files changed

+341
-0
lines changed

.github/workflows/go.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
branches:
9+
- master
10+
jobs:
11+
build_and_test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
- name: Setup Go
17+
uses: actions/setup-go@v5
18+
with:
19+
go-version: 'stable'
20+
check-latest: true
21+
- name: Setup dependency
22+
run: go mod tidy
23+
- name: Test
24+
run: go test -v ./internal/...

.gitignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Code coverage profiles and other test artifacts
15+
*.out
16+
coverage.*
17+
*.coverprofile
18+
profile.cov
19+
20+
# Dependency directories (remove the comment below to include it)
21+
# vendor/
22+
23+
# Go workspace file
24+
go.work
25+
go.work.sum
26+
27+
# env file
28+
.env
29+
30+
# Editor/IDE
31+
# .idea/
32+
# .vscode/
33+
bin

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
build:
2+
@CGO_ENABLED=0 GOOS=linux go build -o bin/main cmd/main.go
3+
4+
run: build
5+
@./bin/main
6+
7+
coverage:
8+
@go test -v -cover ./internal/...
9+
10+
test:
11+
@go test -v ./internal/...

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# golang-graceful-shutdown
2+
3+
This repository is to demo how to implement graceful shutdown with golang
4+
5+
# concept
6+
7+
Why Graceful shutdwon ?
8+
9+
1. Release Resource Properly
10+
2. Prevent Memory Leak for unexpected interrupt
11+
12+
13+
# technical design
14+
15+
## seperate create http server struct and run server
16+
17+
1. Build
18+
```golang
19+
type Server struct {
20+
server *http.Server
21+
}
22+
23+
func NewServer() *Server {
24+
mux := http.NewServeMux()
25+
26+
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
27+
log.Println("Slow request started...")
28+
time.Sleep(8 * time.Second)
29+
fmt.Fprintf(w, "Slow request completed at %v\n", time.Now())
30+
})
31+
return &Server{
32+
server: &http.Server{
33+
Addr: ":8080",
34+
Handler: mux,
35+
},
36+
}
37+
}
38+
```
39+
40+
2. Run
41+
```golang
42+
func (s *Server) Run(ctx context.Context,
43+
shutdownTimeout time.Duration,
44+
) error {
45+
// Run logic
46+
}
47+
```
48+
49+
3. Setup channel for signal system interupt and SIGTERM
50+
51+
```golang
52+
// just catch any signal below
53+
stop := make(chan os.Signal, 1)
54+
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
55+
56+
select {
57+
case err := <-serverErr:
58+
return err
59+
case <-stop:
60+
log.Println("Shutdown signal received")
61+
case <-ctx.Done():
62+
log.Println("Context cancelled")
63+
}
64+
```
65+
66+
4. Shutdown with timeout
67+
```golang
68+
shutdownCtx, cancel := context.WithTimeout(
69+
context.Background(), // prevent inheriant cancellation
70+
shutdownTimeout,
71+
)
72+
defer cancel() // clear up resource
73+
74+
if err := s.server.Shutdown(shutdownCtx); err != nil {
75+
if closeErr := s.server.Close(); closeErr != nil {
76+
return errors.Join(err, closeErr)
77+
}
78+
return err
79+
}
80+
```

cmd/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"time"
9+
10+
"github.com/leetcode-golang-classroom/golang-graceful-shutdown-concept/internal"
11+
)
12+
13+
func main() {
14+
slowResponse := 8 * time.Second
15+
slowRequestHandler := func(w http.ResponseWriter, r *http.Request) {
16+
log.Println("Slow request started...")
17+
time.Sleep(slowResponse)
18+
fmt.Fprintf(w, "Slow request completed at %v\n", time.Now())
19+
}
20+
server := internal.NewServer(":8080", slowRequestHandler)
21+
ctx := context.Background()
22+
shutdownTimeout := 10 * time.Second
23+
if err := server.Run(ctx, shutdownTimeout); err != nil {
24+
log.Fatalf("Server error: %v\n", err)
25+
}
26+
27+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/leetcode-golang-classroom/golang-graceful-shutdown-concept
2+
3+
go 1.24.0

internal/server.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"errors"
6+
"log"
7+
"net/http"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
)
13+
14+
type Server struct {
15+
AppServer *http.Server
16+
}
17+
18+
/*
19+
*
20+
21+
func(w http.ResponseWriter, r *http.Request) {
22+
log.Println("Slow request started...")
23+
time.Sleep(slowResponse)
24+
fmt.Fprintf(w, "Slow request completed at %v\n", time.Now())
25+
}
26+
*/
27+
func NewServer(addr string, slowHandler func(w http.ResponseWriter, r *http.Request)) *Server {
28+
mux := http.NewServeMux()
29+
30+
mux.HandleFunc("/slow", slowHandler)
31+
srv := &Server{
32+
AppServer: &http.Server{
33+
Addr: addr,
34+
Handler: mux,
35+
},
36+
}
37+
return srv
38+
}
39+
40+
func (s *Server) Run(ctx context.Context,
41+
shutdownTimeout time.Duration,
42+
) error {
43+
// buffer channel for not block for receive signal
44+
serverErr := make(chan error, 1)
45+
go func() {
46+
log.Println("Starting server...")
47+
if err := s.AppServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
48+
serverErr <- err
49+
}
50+
close(serverErr)
51+
}()
52+
53+
// just catch any signal below
54+
stop := make(chan os.Signal, 1)
55+
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
56+
57+
select {
58+
case err := <-serverErr:
59+
return err
60+
case <-stop:
61+
log.Println("Shutdown signal received")
62+
case <-ctx.Done():
63+
log.Println("Context cancelled")
64+
}
65+
66+
shutdownCtx, cancel := context.WithTimeout(
67+
context.Background(), // prevent inheriant cancellation
68+
shutdownTimeout,
69+
)
70+
defer cancel() // clear up resource
71+
72+
if err := s.AppServer.Shutdown(shutdownCtx); err != nil {
73+
if closeErr := s.AppServer.Close(); closeErr != nil {
74+
return errors.Join(err, closeErr)
75+
}
76+
return err
77+
}
78+
79+
log.Println("Server exited gracefully")
80+
return nil
81+
}

internal/server_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package internal_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"net/http"
8+
"syscall"
9+
"testing"
10+
"time"
11+
12+
"github.com/leetcode-golang-classroom/golang-graceful-shutdown-concept/internal"
13+
)
14+
15+
func TestServerGracefulShutdown(t *testing.T) {
16+
slowResponse := 2 * time.Second
17+
server := internal.NewServer(":3000", func(w http.ResponseWriter, r *http.Request) {
18+
time.Sleep(slowResponse)
19+
w.Write([]byte("completed"))
20+
})
21+
22+
serverErrorCh := make(chan error)
23+
go func() {
24+
serverErrorCh <- server.Run(context.Background(), 5*time.Second)
25+
}()
26+
time.Sleep(1 * time.Millisecond)
27+
resp, err := http.Get("http://localhost" + server.AppServer.Addr + "/slow")
28+
29+
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
30+
31+
if err != nil {
32+
t.Fatalf("unable to send request to server: %v", err)
33+
}
34+
35+
if resp.StatusCode != http.StatusOK {
36+
t.Errorf("expected 200 StatusOK, got %d", resp.StatusCode)
37+
}
38+
39+
body, err := io.ReadAll(resp.Body)
40+
if err != nil {
41+
t.Fatalf("unable to read body: %v\n", err)
42+
}
43+
44+
if string(body) != "completed" {
45+
t.Errorf("expected body 'completed', got %s", string(body))
46+
}
47+
48+
serverErr := <-serverErrorCh
49+
if serverErr != nil {
50+
t.Fatalf("expected no server error, got %v", serverErr)
51+
}
52+
}
53+
54+
func TestServerTimeoutDuringShutdown(t *testing.T) {
55+
slowResponse := 10 * time.Second
56+
server := internal.NewServer(":3001", func(w http.ResponseWriter, r *http.Request) {
57+
time.Sleep(slowResponse)
58+
w.Write([]byte("completed"))
59+
})
60+
61+
serverErrorCh := make(chan error)
62+
go func() {
63+
serverErrorCh <- server.Run(context.Background(), 5*time.Millisecond)
64+
}()
65+
requestErrorCh := make(chan error)
66+
go func() {
67+
_, err := http.Get("http://localhost" + server.AppServer.Addr + "/slow")
68+
requestErrorCh <- err
69+
}()
70+
71+
time.Sleep(1 * time.Second)
72+
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
73+
74+
if <-requestErrorCh == nil {
75+
t.Errorf("expected client request to fail, but it successded")
76+
}
77+
78+
serverErr := <-serverErrorCh
79+
if !errors.Is(serverErr, context.DeadlineExceeded) {
80+
t.Errorf("expected 'context.DeadlineExceeded' error, got %v", serverErr)
81+
}
82+
}

0 commit comments

Comments
 (0)