├── .gitattributes ├── .env-example ├── main.go ├── .editorconfig ├── readme.md ├── routes.go ├── initialize.go ├── Gopkg.toml ├── .gitignore ├── Gopkg.lock ├── listener.go ├── handlers.go └── server.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | PORT=9000 2 | COMPRESS=true 3 | DEBUG=false 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | initialize() 5 | 6 | // setup and run server 7 | s := newServer() 8 | s.Routes() 9 | s.Run() 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.sql] 16 | indent_style = space 17 | indent_size = 2 18 | end_of_line = lf 19 | charset = utf-8 20 | trim_trailing_whitespace = true 21 | insert_final_newline = true 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This example application is built on fasthttp and fasthttprouter - it is designed to be exceptionally fast, and use very little memory. It is also meant to be an example of a graceful shutdown with fasthttp. Your review, feedback and pull requests are welcome. Cheers! 2 | 3 | Note: assumes you have Dep installed. 4 | 5 | To run clone the repo and run `dep ensure` then `go run $(ls *.go | grep -v _test.go)` (assuming I write tests at some point) 6 | 7 | Resources: 8 | 9 | * https://github.com/valyala/fasthttp 10 | * https://github.com/buaazp/fasthttprouter 11 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | /* 2 | I like to have a single file inside every component called routes.go 3 | where all the routing can live. This is handy because most code 4 | maintenance starts with a URL and an error report — so one glance at 5 | routes.go will direct us where to look. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "github.com/valyala/fasthttp/expvarhandler" 12 | ) 13 | 14 | func (s *server) Routes() { 15 | 16 | // Basics 17 | s.router.GET("/", s.index) 18 | s.router.GET("/long", s.longRunning) 19 | 20 | // Kubernetes 21 | s.router.GET("/healthz", s.healthz) 22 | s.router.GET("/readyz", s.readyz) 23 | 24 | // Monitoring 25 | s.router.GET("/stats", expvarhandler.ExpvarHandler) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /initialize.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/caarlos0/env" 9 | 10 | _ "github.com/joho/godotenv/autoload" // load env vars 11 | ) 12 | 13 | var ( 14 | cfg config 15 | ) 16 | 17 | type config struct { 18 | Compress bool `env:"COMPRESS"` 19 | Debug bool `env:"DEBUG"` 20 | Port string `env:"PORT"` 21 | } 22 | 23 | func initialize() { 24 | // NOTE: For development, github.com/joho/godotenv/autoload 25 | // loads env variables from .env file for you. 26 | 27 | // Read configuration from env variables 28 | err := env.Parse(&cfg) 29 | if err != nil { 30 | fmt.Printf("%+v\n", err) 31 | } 32 | 33 | // log environment variables 34 | if cfg.Debug { 35 | prettyCfg, _ := json.MarshalIndent(cfg, "", " ") 36 | log.Printf("Configuration: \n%v", string(prettyCfg)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/buaazp/fasthttprouter" 30 | version = "0.1.1" 31 | 32 | [[constraint]] 33 | name = "github.com/caarlos0/env" 34 | version = "3.3.0" 35 | 36 | [[constraint]] 37 | name = "github.com/gomodule/redigo" 38 | version = "2.0.0" 39 | 40 | [[constraint]] 41 | name = "github.com/joho/godotenv" 42 | version = "1.2.0" 43 | 44 | [[constraint]] 45 | name = "github.com/json-iterator/go" 46 | version = "1.1.3" 47 | 48 | [[constraint]] 49 | name = "github.com/pkg/errors" 50 | version = "0.8.0" 51 | 52 | [[constraint]] 53 | name = "github.com/valyala/fasthttp" 54 | version = "20160617.0.0" 55 | 56 | [prune] 57 | go-tests = true 58 | unused-packages = true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Assets 2 | # ----------------------------------------------------------------- 3 | *.exe 4 | 5 | # Vendor Files 6 | # ----------------------------------------------------------------- 7 | vendor 8 | 9 | # Postgres 10 | # ----------------------------------------------------------------- 11 | data 12 | 13 | # Configuration 14 | # ---------------------------------------------------------------- 15 | config.json 16 | config.yaml 17 | env.json 18 | .env 19 | users.json 20 | 21 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 22 | # ----------------------------------------------------------------- 23 | *.o 24 | *.a 25 | *.so 26 | 27 | # Folders 28 | # ----------------------------------------------------------------- 29 | _obj 30 | _test 31 | 32 | # Architecture specific extensions/prefixes 33 | # ---------------------------------------------------------------- 34 | *.[568vq] 35 | [568vq].out 36 | *.cgo1.go 37 | *.cgo2.c 38 | _cgo_defun.c 39 | _cgo_gotypes.go 40 | _cgo_export.* 41 | _testmain.go 42 | *.exe 43 | *.test 44 | *.prof 45 | 46 | # Always-ignore extensions 47 | # ---------------------------------------------------------------- 48 | *.diff 49 | *.err 50 | *.orig 51 | *.log 52 | *.rej 53 | *.swo 54 | *.swp 55 | *.zip 56 | *.vi 57 | *~ 58 | *.sass-cache 59 | *.ruby-version 60 | 61 | # OS/Editor 62 | # ---------------------------------------------------------------- 63 | .DS_Store 64 | ._* 65 | Thumbs.db 66 | .cache 67 | .project 68 | .settings 69 | .tmproj 70 | *.esproj 71 | nbproject 72 | *.sublime-project 73 | *.sublime-workspace 74 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/buaazp/fasthttprouter" 6 | packages = ["."] 7 | revision = "ade4e2031af3aed7fffd241084aad80a58faf421" 8 | version = "v0.1.1" 9 | 10 | [[projects]] 11 | name = "github.com/caarlos0/env" 12 | packages = ["."] 13 | revision = "1cddc31c48c56ecd700d873edb9fd5b6f5df922a" 14 | version = "v3.3.0" 15 | 16 | [[projects]] 17 | name = "github.com/gomodule/redigo" 18 | packages = [ 19 | "internal", 20 | "redis" 21 | ] 22 | revision = "9c11da706d9b7902c6da69c592f75637793fe121" 23 | version = "v2.0.0" 24 | 25 | [[projects]] 26 | name = "github.com/joho/godotenv" 27 | packages = [ 28 | ".", 29 | "autoload" 30 | ] 31 | revision = "a79fa1e548e2c689c241d10173efd51e5d689d5b" 32 | version = "v1.2.0" 33 | 34 | [[projects]] 35 | name = "github.com/json-iterator/go" 36 | packages = ["."] 37 | revision = "ca39e5af3ece67bbcda3d0f4f56a8e24d9f2dad4" 38 | version = "1.1.3" 39 | 40 | [[projects]] 41 | name = "github.com/klauspost/compress" 42 | packages = [ 43 | "flate", 44 | "gzip", 45 | "zlib" 46 | ] 47 | revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf" 48 | version = "v1.2.1" 49 | 50 | [[projects]] 51 | name = "github.com/klauspost/cpuid" 52 | packages = ["."] 53 | revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da" 54 | version = "v1.1" 55 | 56 | [[projects]] 57 | name = "github.com/klauspost/crc32" 58 | packages = ["."] 59 | revision = "cb6bfca970f6908083f26f39a79009d608efd5cd" 60 | version = "v1.1" 61 | 62 | [[projects]] 63 | name = "github.com/modern-go/concurrent" 64 | packages = ["."] 65 | revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" 66 | version = "1.0.3" 67 | 68 | [[projects]] 69 | name = "github.com/modern-go/reflect2" 70 | packages = ["."] 71 | revision = "1df9eeb2bb81f327b96228865c5687bc2194af3f" 72 | version = "1.0.0" 73 | 74 | [[projects]] 75 | name = "github.com/pkg/errors" 76 | packages = ["."] 77 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 78 | version = "v0.8.0" 79 | 80 | [[projects]] 81 | name = "github.com/valyala/fasthttp" 82 | packages = [ 83 | ".", 84 | "expvarhandler", 85 | "fasthttputil", 86 | "reuseport" 87 | ] 88 | revision = "d42167fd04f636e20b005e9934159e95454233c7" 89 | version = "v20160617" 90 | 91 | [solve-meta] 92 | analyzer-name = "dep" 93 | analyzer-version = 1 94 | inputs-digest = "681675a000b8318f259e8f72cc2600ce85c0151b9f160ca0ed2cdef1a47a427b" 95 | solver-name = "gps-cdcl" 96 | solver-version = 1 97 | -------------------------------------------------------------------------------- /listener.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | // GracefulListener defines a listener that we can gracefully stop 11 | type GracefulListener struct { 12 | // inner listener 13 | ln net.Listener 14 | 15 | // maximum wait time for graceful shutdown 16 | maxWaitTime time.Duration 17 | 18 | // this channel is closed during graceful shutdown on zero open connections. 19 | done chan struct{} 20 | 21 | // the number of open connections 22 | connsCount uint64 23 | 24 | // becomes non-zero when graceful shutdown starts 25 | shutdown uint64 26 | } 27 | 28 | // NewGracefulListener wraps the given listener into 'graceful shutdown' listener. 29 | func NewGracefulListener(ln net.Listener, maxWaitTime time.Duration) net.Listener { 30 | return &GracefulListener{ 31 | ln: ln, 32 | maxWaitTime: maxWaitTime, 33 | done: make(chan struct{}), 34 | } 35 | } 36 | 37 | // Accept creates a conn 38 | func (ln *GracefulListener) Accept() (net.Conn, error) { 39 | c, err := ln.ln.Accept() 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | atomic.AddUint64(&ln.connsCount, 1) 46 | 47 | return &gracefulConn{ 48 | Conn: c, 49 | ln: ln, 50 | }, nil 51 | } 52 | 53 | // Addr returns the listen address 54 | func (ln *GracefulListener) Addr() net.Addr { 55 | return ln.ln.Addr() 56 | } 57 | 58 | // Close closes the inner listener and waits until all the pending 59 | // open connections are closed before returning. 60 | func (ln *GracefulListener) Close() error { 61 | err := ln.ln.Close() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return ln.waitForZeroConns() 67 | } 68 | 69 | func (ln *GracefulListener) waitForZeroConns() error { 70 | atomic.AddUint64(&ln.shutdown, 1) 71 | 72 | if atomic.LoadUint64(&ln.connsCount) == 0 { 73 | close(ln.done) 74 | return nil 75 | } 76 | 77 | select { 78 | case <-ln.done: 79 | return nil 80 | case <-time.After(ln.maxWaitTime): 81 | return fmt.Errorf("cannot complete graceful shutdown in %s", ln.maxWaitTime) 82 | } 83 | } 84 | 85 | func (ln *GracefulListener) closeConn() { 86 | connsCount := atomic.AddUint64(&ln.connsCount, ^uint64(0)) 87 | 88 | if atomic.LoadUint64(&ln.shutdown) != 0 && connsCount == 0 { 89 | close(ln.done) 90 | } 91 | } 92 | 93 | type gracefulConn struct { 94 | net.Conn 95 | ln *GracefulListener 96 | } 97 | 98 | func (c *gracefulConn) Close() error { 99 | err := c.Conn.Close() 100 | 101 | if err != nil { 102 | return err 103 | } 104 | 105 | c.ln.closeConn() 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | // NOTE: handlers hang off the server: 10 | // func (s *server) handler(ctx *fasthttp.RequestCtx) { ... } 11 | // That way handlers can access the dependencies via the s server variable. 12 | 13 | var ( 14 | done = []byte(`{"done": true}`) 15 | ready = []byte(`{"ready": true}`) 16 | healthy = []byte(`{"alive": true}`) 17 | html = []byte(` 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 42 | 43 | `) 44 | ) 45 | 46 | func (s *server) index(ctx *fasthttp.RequestCtx) { 47 | ctx.SetContentType("text/html") 48 | ctx.SetBody(html) 49 | } 50 | 51 | // longRunning is used to test our graceful shutdown 52 | func (s *server) longRunning(ctx *fasthttp.RequestCtx) { 53 | time.Sleep(2 * time.Second) // simulate long process 54 | ctx.SetContentType("application/json") 55 | ctx.SetBody(done) 56 | } 57 | 58 | // healthz supports a health probe. It is a simple handler which 59 | // always return response code 200 @ /healthz 60 | func (s *server) healthz(ctx *fasthttp.RequestCtx) { 61 | ctx.SetContentType("application/json") 62 | ctx.SetBody(healthy) 63 | } 64 | 65 | // readyz supports a readiness probe. For the readiness probe we might 66 | // need to wait for some event (e.g. the database is ready) to be able 67 | // to serve traffic. Returns 200 @ readyz 68 | func (s *server) readyz(ctx *fasthttp.RequestCtx) { 69 | ctx.SetContentType("application/json") 70 | ctx.SetBody(ready) 71 | 72 | // NOTE: A lot of additional useful info is exposed to request handler: 73 | // fmt.Fprintf(ctx, "Request method is %q\n", ctx.Method()) 74 | // fmt.Fprintf(ctx, "Request ID is %v\n", ctx.ID()) 75 | // fmt.Fprintf(ctx, "RequestURI is %q\n", ctx.RequestURI()) 76 | // fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path()) 77 | // fmt.Fprintf(ctx, "Host is %q\n", ctx.Host()) 78 | // fmt.Fprintf(ctx, "Query string is %q\n", ctx.QueryArgs()) 79 | // fmt.Fprintf(ctx, "User-Agent is %q\n", ctx.UserAgent()) 80 | // fmt.Fprintf(ctx, "Connection has been established at %s\n", ctx.ConnTime()) 81 | // fmt.Fprintf(ctx, "Request has been started at %s\n", ctx.Time()) 82 | // fmt.Fprintf(ctx, "Serial request number for the current connection is %d\n", ctx.ConnRequestNum()) 83 | // fmt.Fprintf(ctx, "Your ip is %q\n\n", ctx.RemoteIP()) 84 | // fmt.Fprintf(ctx, "Raw request is:\n---CUT---\n%s\n---CUT---", &ctx.Request) 85 | } 86 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/buaazp/fasthttprouter" 12 | "github.com/valyala/fasthttp" 13 | "github.com/valyala/fasthttp/reuseport" 14 | ) 15 | 16 | type server struct { 17 | HTTPServer *fasthttp.Server 18 | router *fasthttprouter.Router 19 | } 20 | 21 | // NewServer creates a new HTTP Server 22 | func newServer() *server { 23 | 24 | // define router 25 | r := fasthttprouter.New() 26 | 27 | // compression 28 | h := r.Handler 29 | if cfg.Compress { 30 | h = fasthttp.CompressHandler(h) 31 | } 32 | 33 | return &server{ 34 | HTTPServer: newHTTPServer(h), 35 | router: r, 36 | } 37 | } 38 | 39 | // NewServer creates a new HTTP Server 40 | // TODO: configuration should be configurable 41 | func newHTTPServer(h fasthttp.RequestHandler) *fasthttp.Server { 42 | return &fasthttp.Server{ 43 | Handler: h, 44 | ReadTimeout: 5 * time.Second, 45 | WriteTimeout: 10 * time.Second, 46 | MaxConnsPerIP: 500, 47 | MaxRequestsPerConn: 500, 48 | MaxKeepaliveDuration: 5 * time.Second, 49 | } 50 | } 51 | 52 | // Run starts the HTTP server and performs a graceful shutdown 53 | func (s *server) Run() { 54 | // NOTE: Package reuseport provides a TCP net.Listener with SO_REUSEPORT support. 55 | // SO_REUSEPORT allows linear scaling server performance on multi-CPU servers. 56 | 57 | // create a fast listener ;) 58 | ln, err := reuseport.Listen("tcp4", "localhost:"+cfg.Port) 59 | if err != nil { 60 | log.Fatalf("error in reuseport listener: %s", err) 61 | } 62 | 63 | // create a graceful shutdown listener 64 | duration := 5 * time.Second 65 | graceful := NewGracefulListener(ln, duration) 66 | 67 | // Get hostname 68 | hostname, err := os.Hostname() 69 | if err != nil { 70 | log.Fatalf("hostname unavailable: %s", err) 71 | } 72 | 73 | // Error handling 74 | listenErr := make(chan error, 1) 75 | 76 | /// Run server 77 | go func() { 78 | log.Printf("%s - Web server starting on port %v", hostname, graceful.Addr()) 79 | log.Printf("%s - Press Ctrl+C to stop", hostname) 80 | // listenErr <- s.HTTPServer.ListenAndServe(":" + cfg.Port) 81 | listenErr <- s.HTTPServer.Serve(graceful) 82 | 83 | }() 84 | 85 | // SIGINT/SIGTERM handling 86 | osSignals := make(chan os.Signal, 1) 87 | signal.Notify(osSignals, syscall.SIGINT, syscall.SIGTERM) 88 | 89 | // Handle channels/graceful shutdown 90 | for { 91 | select { 92 | // If server.ListenAndServe() cannot start due to errors such 93 | // as "port in use" it will return an error. 94 | case err := <-listenErr: 95 | if err != nil { 96 | log.Fatalf("listener error: %s", err) 97 | } 98 | os.Exit(0) 99 | // handle termination signal 100 | case <-osSignals: 101 | fmt.Printf("\n") 102 | log.Printf("%s - Shutdown signal received.\n", hostname) 103 | 104 | // Servers in the process of shutting down should disable KeepAlives 105 | // FIXME: This causes a data race 106 | s.HTTPServer.DisableKeepalive = true 107 | 108 | // Attempt the graceful shutdown by closing the listener 109 | // and completing all inflight requests. 110 | if err := graceful.Close(); err != nil { 111 | log.Fatalf("error with graceful close: %s", err) 112 | } 113 | 114 | log.Printf("%s - Server gracefully stopped.\n", hostname) 115 | } 116 | } 117 | } 118 | --------------------------------------------------------------------------------