-
Star
(139)
You must be signed in to star a gist -
Fork
(35)
You must be signed in to fork a gist
-
-
Save peterhellberg/38117e546c217960747aacf689af3dc2 to your computer and use it in GitHub Desktop.
package main | |
import ( | |
"context" | |
"log" | |
"net/http" | |
"os" | |
"os/signal" | |
"time" | |
) | |
type Server struct { | |
logger *log.Logger | |
mux *http.ServeMux | |
} | |
func NewServer(options ...func(*Server)) *Server { | |
s := &Server{ | |
logger: log.New(os.Stdout, "", 0), | |
mux: http.NewServeMux(), | |
} | |
for _, f := range options { | |
f(s) | |
} | |
s.mux.HandleFunc("/", s.index) | |
return s | |
} | |
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
s.mux.ServeHTTP(w, r) | |
} | |
func (s *Server) index(w http.ResponseWriter, r *http.Request) { | |
s.logger.Println("GET /") | |
w.Write([]byte("Hello, World!")) | |
} | |
func main() { | |
stop := make(chan os.Signal, 1) | |
signal.Notify(stop, os.Interrupt) | |
logger := log.New(os.Stdout, "", 0) | |
addr := ":" + os.Getenv("PORT") | |
if addr == ":" { | |
addr = ":2017" | |
} | |
s := NewServer(func(s *Server) { s.logger = logger }) | |
h := &http.Server{Addr: addr, Handler: s} | |
go func() { | |
logger.Printf("Listening on http://0.0.0.0%s\n", addr) | |
if err := h.ListenAndServe(); err != nil { | |
logger.Fatal(err) | |
} | |
}() | |
<-stop | |
logger.Println("\nShutting down the server...") | |
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) | |
h.Shutdown(ctx) | |
logger.Println("Server gracefully stopped") | |
} |
Using the github.com/TV4/graceful
package
EDIT: This example is actually wrong as pointed out by @pci below, I've kept it as is in order for his comment to make sense.
You probably want to use
graceful.ListenAndServe
orgraceful.LogListenAndServe
instead ofgraceful.Shutdown
I have written the github.com/TV4/graceful package in order to reduce boilerplate code needed to set up a *http.Server
with graceful shutdown.
package main
import (
"log"
"net/http"
"github.com/TV4/graceful"
)
func main() {
hs := &http.Server{Addr: ":2017", Handler: &server{}}
go graceful.Shutdown(hs)
log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
}
@peterhellberg Hey these are really useful, thanks for putting them together. One thing though, I was testing with some slow requests and it seems like: when server.Shutdown()
is called server.ListenAndServe
returns straight away, even if there are ongoing requests, which then means that the main function exits and the requests are still killed early. In testing a request of a second or two is enough to see the effect.
My workaround was to pass a done channel to graceful
so it can report back once the server has shutdown, but would there be any better ways?
Thanks again for these, in my searching they were definitely the best examples online of how to actually use server.Shutdown
in practice.
@pci Yes, you are absolutely correct. And the example I gave above is actually wrong.
If you block on graceful.Shutdown
instead it seems to work as intended:
package main
import (
"log"
"net/http"
"time"
"github.com/TV4/graceful"
)
func main() {
hs := &http.Server{Addr: ":2017", Handler: &server{}}
go func() {
log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
graceful.Shutdown(hs)
}
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello!"))
}
(And if you change the sleep from 2 to 20 seconds you'll hit the graceful.DefaultTimout of 15 seconds and get Error: context deadline exceeded
in the log)
@pci I have now simplified it even further by adding a ListenAndServe
function to the graceful
package.
package main
import (
"log"
"net/http"
"time"
"github.com/TV4/graceful"
)
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello!"))
}
func main() {
addr := ":2017"
log.Printf("Listening on http://0.0.0.0%s\n", addr)
graceful.ListenAndServe(&http.Server{
Addr: addr,
Handler: &server{},
})
}
@pci Since I do logging on the listening URL in almost every service I write I've now also added LogListenAndServe
:
package main
import (
"net/http"
"time"
"github.com/TV4/graceful"
)
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello!"))
}
func main() {
graceful.LogListenAndServe(&http.Server{
Addr: ":2017",
Handler: &server{},
})
}
$ go run example.go
Listening on http://0.0.0.0:2017
^C
Shutdown with timeout: 15s
Server stopped
@peterhellberg Nice additions 👍
hi~~
The line 62 of first demo "graceful.go" "logger.Fatal(err)" maybe lead to last print (" Server gracefully stopped" )not display
@wangjinbei: Yes, this gist mainly contains improvements to the original code in the comments. I didn't want to change it too much in order to not confuse people :)
@peterhellberg Great examples!
Go vet would indicate a leak on the cancel function for the first one on context.WithTimeout.
I got Error: context deadline exceeded
when I first to request http://0.0.0.0:2017
and then shutdown
in Example with graceful function
There are more below lol. Great job!
hey,
I created a package which does all the work for you with a simple api:
https://github.com/pseidemann/finish
let me know what you think!
@peterhellberg Thanks for posting these examples. Really helpful.
@peterhellberg Thanks for sharing this!
Hi, great contribution.
There is one thing that could make it more complete in my opinion.
Everywhere this code is, it will always print "Listening on http://...." in the console, even if ListenAndServe()
was unable to start actually listening.
log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
If you start 1 server, you'll see this in the console:
Listening on http://0.0.0.0:2017
If you start a 2nd server without shutting down the 1st one, you will see this in the console:
Listening on http://0.0.0.0:2017
panic... listen tcp :2017: bind: address already in use
So it first shows "Listening on http://..." however this never actually happened.
I'm not a go expert, is there a way to implement this?
-- UPDATE
Apparently using http.Listen
and http.Serve
individually, it is possible to catch a port in use error (or any other error while trying to listen to a tcp network) before assuming the server is listening.
Source: https://stackoverflow.com/a/48250354/194630
listener, err := net.Listen("tcp", ":"+config.Port)
if err != nil {
log.Fatal(err)
}
done := make(chan bool)
go server.Serve(listener)
// Log server started
log.Printf("Server started at port %v", config.Port)
<-done
Example with separate
main
andserver
packagesEDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it
main.go
server/server.go
And now you should be able to
curl http://0.0.0.0:2017/hello/yourname