Skip to content

Commit 8042f18

Browse files
committed
Add material for GoLab 2020 ws
1 parent 222b91b commit 8042f18

78 files changed

Lines changed: 8030 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Design patterns for production grade Go services
2+
## Material for the GoLab 2020 Workshop
3+
4+
## Prerequisites
5+
6+
As an intermediate workshop, familiarity with the basics of the Go programming language is expected. As a reference, you have already completed (or should be able to complete) [A Tour of Go](https://tour.golang.org/).
7+
8+
A previous experience in writing Go services may be useful, but not required.
9+
10+
To install Go 1.15 and configure your environment properly, follow the instructions [here](https://golang.org/doc/install).
11+
12+
During the workshop, we will try the example code with some HTTP requests. `curl` is perfectly fine, but it may be easier to use a tool like [Insomnia Core](https://insomnia.rest/download/core/?) or [Postman](https://www.postman.com/downloads/).
13+
14+
## Setup test
15+
16+
see [this repo](https://github.com/pippolo84/golab2020-ws-setup-test) for more information regarding your local dev environment setup

part1/graceful-shutdown/go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/Pippolo84/go-services-patterns/part1/graceful-shutdown
2+
3+
go 1.15
4+
5+
require (
6+
go.uber.org/goleak v1.1.10
7+
nhooyr.io/websocket v1.8.6
8+
)

part1/graceful-shutdown/go.sum

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
4+
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
5+
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
6+
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
7+
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
8+
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
9+
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
10+
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
11+
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
12+
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
13+
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
14+
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
15+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
16+
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
17+
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
18+
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
19+
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
20+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
21+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
22+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
23+
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
24+
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
25+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
26+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
27+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
28+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
29+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
30+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
31+
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
32+
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
33+
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
34+
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
35+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
36+
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
37+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
38+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
39+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
40+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
41+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
43+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
44+
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
45+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
46+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
47+
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
48+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
49+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
50+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
51+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
52+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
53+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
54+
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
55+
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package graceful
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"net"
9+
"net/http"
10+
"sync"
11+
"time"
12+
13+
"nhooyr.io/websocket"
14+
)
15+
16+
// Server is a HTTP server that supports graceful shutdown
17+
type Server struct {
18+
http.Server
19+
}
20+
21+
// NewServer returns a reference to a new Server listening on addr
22+
func NewServer(addr string) *Server {
23+
mux := http.NewServeMux()
24+
25+
mux.HandleFunc("/", handler)
26+
mux.HandleFunc("/slow", slowHandler)
27+
// see https://github.com/hashrocket/ws for a websocket client
28+
mux.HandleFunc("/ws", wsHandler)
29+
30+
ctx, cancel := context.WithCancel(context.Background())
31+
srv := &Server{
32+
Server: http.Server{
33+
Addr: addr,
34+
Handler: mux,
35+
// use the context with cancellation to be able to gracefully shutdown websocket connections
36+
BaseContext: func(_ net.Listener) context.Context {
37+
return ctx
38+
},
39+
},
40+
}
41+
42+
// on shutdown, cancel the context
43+
srv.Server.RegisterOnShutdown(cancel)
44+
45+
return srv
46+
}
47+
48+
// Shutdown makes the server stop listening and refuse further connections
49+
// It takes a context to limit the shutdown duration and a wait group to signal
50+
// the caller when the shutdown process has finished
51+
func (srv *Server) Shutdown(ctx context.Context, wg *sync.WaitGroup) error {
52+
defer wg.Done()
53+
return srv.Server.Shutdown(ctx)
54+
}
55+
56+
// Run starts the server, making it listening on specified address
57+
// it returns a channel where all errors are relayed
58+
func (srv *Server) Run() <-chan error {
59+
errs := make(chan error)
60+
61+
go func() {
62+
defer close(errs)
63+
64+
log.Printf("server: start listening on %s\n", srv.Addr)
65+
66+
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
67+
errs <- err
68+
}
69+
70+
log.Println("server: bye!")
71+
}()
72+
73+
return errs
74+
}
75+
76+
func handler(w http.ResponseWriter, r *http.Request) {
77+
fmt.Fprintf(w, "Hello, world!")
78+
}
79+
80+
func slowHandler(w http.ResponseWriter, r *http.Request) {
81+
// Shutdown won't close this connection until it returns to idle
82+
time.Sleep(10 * time.Second)
83+
fmt.Fprintf(w, "Hello, slow world!")
84+
}
85+
86+
func wsHandler(w http.ResponseWriter, r *http.Request) {
87+
c, err := websocket.Accept(w, r, nil)
88+
if err != nil {
89+
fmt.Printf("websocket upgrade error: %v\n", err)
90+
}
91+
// Additional calls to Close are no-ops
92+
defer c.Close(websocket.StatusInternalError, "internal server error")
93+
94+
// to cancel the write in case of a slow reader
95+
ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
96+
defer cancel()
97+
98+
if err := c.Write(ctx, websocket.MessageText, []byte("Hello, ws world!")); err != nil {
99+
if !errors.Is(err, context.Canceled) {
100+
log.Printf("ws write error: %v\n", err)
101+
}
102+
return
103+
}
104+
105+
mtype, buf, err := c.Read(ctx)
106+
if err != nil {
107+
if !errors.Is(err, context.Canceled) {
108+
log.Printf("ws read error: %v\n", err)
109+
}
110+
return
111+
}
112+
113+
if mtype == websocket.MessageText {
114+
fmt.Printf("received: %s\n", string(buf))
115+
}
116+
117+
c.Close(websocket.StatusNormalClosure, "")
118+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package graceful
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"net"
9+
"net/http"
10+
"strings"
11+
"sync"
12+
"testing"
13+
"time"
14+
15+
"go.uber.org/goleak"
16+
)
17+
18+
// FIXME: add a test for the ws handler
19+
20+
// go test -v -timeout=20s .
21+
22+
func startServer(t *testing.T, addr string) (*Server, <-chan error) {
23+
t.Helper()
24+
25+
srv := NewServer(addr)
26+
27+
errs := make(chan error)
28+
29+
go func() {
30+
defer close(errs)
31+
32+
for err := range srv.Run() {
33+
errs <- err
34+
}
35+
}()
36+
37+
// wait for the server to listen on addr
38+
for {
39+
_, err := net.Dial("tcp", fmt.Sprintf("localhost%s", addr))
40+
if err == nil {
41+
break
42+
}
43+
if strings.Contains(err.Error(), "connection refused") {
44+
time.Sleep(100 * time.Millisecond)
45+
continue
46+
}
47+
48+
log.Fatal(err)
49+
}
50+
51+
return srv, errs
52+
}
53+
54+
func TestGetRequest(t *testing.T) {
55+
defer goleak.VerifyNone(t)
56+
57+
srv, srvErrs := startServer(t, ":8080")
58+
59+
res, err := http.Get("http://localhost:8080")
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
defer res.Body.Close()
64+
65+
if res.StatusCode != http.StatusOK {
66+
t.Fatalf("expected status code %d, got %d\n", http.StatusOK, res.StatusCode)
67+
}
68+
69+
var wg sync.WaitGroup
70+
wg.Add(1)
71+
72+
if err := srv.Shutdown(context.Background(), &wg); err != nil {
73+
t.Fatal(err)
74+
}
75+
76+
wg.Wait()
77+
78+
// srvErrs should have been closed without errors
79+
for err := range srvErrs {
80+
t.Fatal(err)
81+
}
82+
}
83+
84+
func TestGracefulShutdown(t *testing.T) {
85+
defer goleak.VerifyNone(t)
86+
87+
srv, srvErrs := startServer(t, ":8080")
88+
89+
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 20*time.Second)
90+
defer cancelShutdown()
91+
92+
var wg sync.WaitGroup
93+
wg.Add(1)
94+
95+
if err := srv.Shutdown(shutdownCtx, &wg); err != nil {
96+
t.Fatal(err)
97+
}
98+
99+
wg.Wait()
100+
101+
// srvErrs should have been closed without errors
102+
for err := range srvErrs {
103+
t.Fatal(err)
104+
}
105+
}
106+
107+
func slowRequest(t *testing.T, addr string) <-chan error {
108+
t.Helper()
109+
110+
reqErrs := make(chan error)
111+
112+
go func() {
113+
defer close(reqErrs)
114+
115+
var err *net.OpError
116+
if _, e := http.Get(addr); !errors.As(e, &err) {
117+
reqErrs <- err
118+
}
119+
}()
120+
121+
return reqErrs
122+
}
123+
124+
func TestGracefulShutdownTimeout(t *testing.T) {
125+
srv, srvErrs := startServer(t, ":8081")
126+
127+
reqErrs := slowRequest(t, "http://localhost:8081/slow")
128+
129+
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), time.Second)
130+
defer cancelShutdown()
131+
132+
var wg sync.WaitGroup
133+
wg.Add(1)
134+
135+
if err := srv.Shutdown(shutdownCtx, &wg); !errors.Is(err, context.DeadlineExceeded) {
136+
t.Fatalf("expected context deadline exceeded, got: %v\n", err)
137+
}
138+
139+
wg.Wait()
140+
141+
// srvErrs and reqErrs should have been closed without errors
142+
for err := range srvErrs {
143+
t.Fatal(err)
144+
}
145+
for err := range reqErrs {
146+
t.Fatal(err)
147+
}
148+
}

part1/graceful-shutdown/main.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"os"
7+
"os/signal"
8+
"sync"
9+
"syscall"
10+
"time"
11+
12+
"github.com/Pippolo84/go-services-patterns/part1/graceful-shutdown/graceful"
13+
)
14+
15+
const cooldown time.Duration = 5 * time.Second
16+
17+
func main() {
18+
srv := graceful.NewServer(":8080")
19+
20+
errs := srv.Run()
21+
22+
// trap incoming SIGINT and SIGKTERM
23+
signalChan := make(chan os.Signal, 1)
24+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
25+
26+
// block until a signal or an error from the server is received
27+
select {
28+
case err := <-errs:
29+
log.Println(err)
30+
case sig := <-signalChan:
31+
log.Printf("got signal: %v, shutting down...\n", sig)
32+
}
33+
34+
// graceful shutdown the server
35+
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), cooldown)
36+
defer cancelShutdown()
37+
38+
var wg sync.WaitGroup
39+
wg.Add(1)
40+
41+
if err := srv.Shutdown(shutdownCtx, &wg); err != nil {
42+
log.Println(err)
43+
}
44+
45+
wg.Wait()
46+
}

0 commit comments

Comments
 (0)