You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							395 lines
						
					
					
						
							9.8 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							395 lines
						
					
					
						
							9.8 KiB
						
					
					
				| // Package httpdown provides http.ConnState enabled graceful termination of | |
| // http.Server. | |
| // based on github.com/facebookarchive/httpdown, who's licence is MIT-licence, | |
| // we add a feature of supporting for http TLS | |
| package httpdown | |
| 
 | |
| import ( | |
| 	"crypto/tls" | |
| 	"fmt" | |
| 	"net" | |
| 	"net/http" | |
| 	"os" | |
| 	"os/signal" | |
| 	"sync" | |
| 	"syscall" | |
| 	"time" | |
| 
 | |
| 	"github.com/facebookgo/clock" | |
| 	"github.com/facebookgo/stats" | |
| ) | |
| 
 | |
| const ( | |
| 	defaultStopTimeout = time.Minute | |
| 	defaultKillTimeout = time.Minute | |
| ) | |
| 
 | |
| // A Server allows encapsulates the process of accepting new connections and | |
| // serving them, and gracefully shutting down the listener without dropping | |
| // active connections. | |
| type Server interface { | |
| 	// Wait waits for the serving loop to finish. This will happen when Stop is | |
| 	// called, at which point it returns no error, or if there is an error in the | |
| 	// serving loop. You must call Wait after calling Serve or ListenAndServe. | |
| 	Wait() error | |
| 
 | |
| 	// Stop stops the listener. It will block until all connections have been | |
| 	// closed. | |
| 	Stop() error | |
| } | |
| 
 | |
| // HTTP defines the configuration for serving a http.Server. Multiple calls to | |
| // Serve or ListenAndServe can be made on the same HTTP instance. The default | |
| // timeouts of 1 minute each result in a maximum of 2 minutes before a Stop() | |
| // returns. | |
| type HTTP struct { | |
| 	// StopTimeout is the duration before we begin force closing connections. | |
| 	// Defaults to 1 minute. | |
| 	StopTimeout time.Duration | |
| 
 | |
| 	// KillTimeout is the duration before which we completely give up and abort | |
| 	// even though we still have connected clients. This is useful when a large | |
| 	// number of client connections exist and closing them can take a long time. | |
| 	// Note, this is in addition to the StopTimeout. Defaults to 1 minute. | |
| 	KillTimeout time.Duration | |
| 
 | |
| 	// Stats is optional. If provided, it will be used to record various metrics. | |
| 	Stats stats.Client | |
| 
 | |
| 	// Clock allows for testing timing related functionality. Do not specify this | |
| 	// in production code. | |
| 	Clock clock.Clock | |
| 
 | |
| 	// when set CertFile and KeyFile, the httpDown will start a http with TLS. | |
| 	// Files containing a certificate and matching private key for the | |
| 	// server must be provided if neither the Server's | |
| 	// TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. | |
| 	// If the certificate is signed by a certificate authority, the | |
| 	// certFile should be the concatenation of the server's certificate, | |
| 	// any intermediates, and the CA's certificate. | |
| 	CertFile, KeyFile string | |
| } | |
| 
 | |
| // Serve provides the low-level API which is useful if you're creating your own | |
| // net.Listener. | |
| func (h HTTP) Serve(s *http.Server, l net.Listener) Server { | |
| 	stopTimeout := h.StopTimeout | |
| 	if stopTimeout == 0 { | |
| 		stopTimeout = defaultStopTimeout | |
| 	} | |
| 	killTimeout := h.KillTimeout | |
| 	if killTimeout == 0 { | |
| 		killTimeout = defaultKillTimeout | |
| 	} | |
| 	klock := h.Clock | |
| 	if klock == nil { | |
| 		klock = clock.New() | |
| 	} | |
| 
 | |
| 	ss := &server{ | |
| 		stopTimeout:  stopTimeout, | |
| 		killTimeout:  killTimeout, | |
| 		stats:        h.Stats, | |
| 		clock:        klock, | |
| 		oldConnState: s.ConnState, | |
| 		listener:     l, | |
| 		server:       s, | |
| 		serveDone:    make(chan struct{}), | |
| 		serveErr:     make(chan error, 1), | |
| 		new:          make(chan net.Conn), | |
| 		active:       make(chan net.Conn), | |
| 		idle:         make(chan net.Conn), | |
| 		closed:       make(chan net.Conn), | |
| 		stop:         make(chan chan struct{}), | |
| 		kill:         make(chan chan struct{}), | |
| 		certFile:     h.CertFile, | |
| 		keyFile:      h.KeyFile, | |
| 	} | |
| 	s.ConnState = ss.connState | |
| 	go ss.manage() | |
| 	go ss.serve() | |
| 	return ss | |
| } | |
| 
 | |
| // ListenAndServe returns a Server for the given http.Server. It is equivalent | |
| // to ListenAndServe from the standard library, but returns immediately. | |
| // Requests will be accepted in a background goroutine. If the http.Server has | |
| // a non-nil TLSConfig, a TLS enabled listener will be setup. | |
| func (h HTTP) ListenAndServe(s *http.Server) (Server, error) { | |
| 	addr := s.Addr | |
| 	if addr == "" { | |
| 		if s.TLSConfig == nil { | |
| 			addr = ":http" | |
| 		} else { | |
| 			addr = ":https" | |
| 		} | |
| 	} | |
| 	l, err := net.Listen("tcp", addr) | |
| 	if err != nil { | |
| 		stats.BumpSum(h.Stats, "listen.error", 1) | |
| 		return nil, err | |
| 	} | |
| 	if s.TLSConfig != nil { | |
| 		l = tls.NewListener(l, s.TLSConfig) | |
| 	} | |
| 	return h.Serve(s, l), nil | |
| } | |
| 
 | |
| // server manages the serving process and allows for gracefully stopping it. | |
| type server struct { | |
| 	stopTimeout time.Duration | |
| 	killTimeout time.Duration | |
| 	stats       stats.Client | |
| 	clock       clock.Clock | |
| 
 | |
| 	oldConnState func(net.Conn, http.ConnState) | |
| 	server       *http.Server | |
| 	serveDone    chan struct{} | |
| 	serveErr     chan error | |
| 	listener     net.Listener | |
| 
 | |
| 	new    chan net.Conn | |
| 	active chan net.Conn | |
| 	idle   chan net.Conn | |
| 	closed chan net.Conn | |
| 	stop   chan chan struct{} | |
| 	kill   chan chan struct{} | |
| 
 | |
| 	stopOnce sync.Once | |
| 	stopErr  error | |
| 
 | |
| 	certFile, keyFile string | |
| } | |
| 
 | |
| func (s *server) connState(c net.Conn, cs http.ConnState) { | |
| 	if s.oldConnState != nil { | |
| 		s.oldConnState(c, cs) | |
| 	} | |
| 
 | |
| 	switch cs { | |
| 	case http.StateNew: | |
| 		s.new <- c | |
| 	case http.StateActive: | |
| 		s.active <- c | |
| 	case http.StateIdle: | |
| 		s.idle <- c | |
| 	case http.StateHijacked, http.StateClosed: | |
| 		s.closed <- c | |
| 	} | |
| } | |
| 
 | |
| func (s *server) manage() { | |
| 	defer func() { | |
| 		close(s.new) | |
| 		close(s.active) | |
| 		close(s.idle) | |
| 		close(s.closed) | |
| 		close(s.stop) | |
| 		close(s.kill) | |
| 	}() | |
| 
 | |
| 	var stopDone chan struct{} | |
| 
 | |
| 	conns := map[net.Conn]http.ConnState{} | |
| 	var countNew, countActive, countIdle float64 | |
| 
 | |
| 	// decConn decrements the count associated with the current state of the | |
| 	// given connection. | |
| 	decConn := func(c net.Conn) { | |
| 		switch conns[c] { | |
| 		default: | |
| 			panic(fmt.Errorf("unknown existing connection: %s", c)) | |
| 		case http.StateNew: | |
| 			countNew-- | |
| 		case http.StateActive: | |
| 			countActive-- | |
| 		case http.StateIdle: | |
| 			countIdle-- | |
| 		} | |
| 	} | |
| 
 | |
| 	// setup a ticker to report various values every minute. if we don't have a | |
| 	// Stats implementation provided, we Stop it so it never ticks. | |
| 	statsTicker := s.clock.Ticker(time.Minute) | |
| 	if s.stats == nil { | |
| 		statsTicker.Stop() | |
| 	} | |
| 
 | |
| 	for { | |
| 		select { | |
| 		case <-statsTicker.C: | |
| 			// we'll only get here when s.stats is not nil | |
| 			s.stats.BumpAvg("http-state.new", countNew) | |
| 			s.stats.BumpAvg("http-state.active", countActive) | |
| 			s.stats.BumpAvg("http-state.idle", countIdle) | |
| 			s.stats.BumpAvg("http-state.total", countNew+countActive+countIdle) | |
| 		case c := <-s.new: | |
| 			conns[c] = http.StateNew | |
| 			countNew++ | |
| 		case c := <-s.active: | |
| 			decConn(c) | |
| 			countActive++ | |
| 
 | |
| 			conns[c] = http.StateActive | |
| 		case c := <-s.idle: | |
| 			decConn(c) | |
| 			countIdle++ | |
| 
 | |
| 			conns[c] = http.StateIdle | |
| 
 | |
| 			// if we're already stopping, close it | |
| 			if stopDone != nil { | |
| 				c.Close() | |
| 			} | |
| 		case c := <-s.closed: | |
| 			stats.BumpSum(s.stats, "conn.closed", 1) | |
| 			decConn(c) | |
| 			delete(conns, c) | |
| 
 | |
| 			// if we're waiting to stop and are all empty, we just closed the last | |
| 			// connection and we're done. | |
| 			if stopDone != nil && len(conns) == 0 { | |
| 				close(stopDone) | |
| 				return | |
| 			} | |
| 		case stopDone = <-s.stop: | |
| 			// if we're already all empty, we're already done | |
| 			if len(conns) == 0 { | |
| 				close(stopDone) | |
| 				return | |
| 			} | |
| 
 | |
| 			// close current idle connections right away | |
| 			for c, cs := range conns { | |
| 				if cs == http.StateIdle { | |
| 					c.Close() | |
| 				} | |
| 			} | |
| 
 | |
| 			// continue the loop and wait for all the ConnState updates which will | |
| 			// eventually close(stopDone) and return from this goroutine. | |
|  | |
| 		case killDone := <-s.kill: | |
| 			// force close all connections | |
| 			stats.BumpSum(s.stats, "kill.conn.count", float64(len(conns))) | |
| 			for c := range conns { | |
| 				c.Close() | |
| 			} | |
| 
 | |
| 			// don't block the kill. | |
| 			close(killDone) | |
| 
 | |
| 			// continue the loop and we wait for all the ConnState updates and will | |
| 			// return from this goroutine when we're all done. otherwise we'll try to | |
| 			// send those ConnState updates on closed channels. | |
|  | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| func (s *server) serve() { | |
| 	stats.BumpSum(s.stats, "serve", 1) | |
| 	if s.certFile == "" && s.keyFile == "" { | |
| 		s.serveErr <- s.server.Serve(s.listener) | |
| 	} else { | |
| 		s.serveErr <- s.server.ServeTLS(s.listener, s.certFile, s.keyFile) | |
| 	} | |
| 	close(s.serveDone) | |
| 	close(s.serveErr) | |
| } | |
| 
 | |
| func (s *server) Wait() error { | |
| 	if err := <-s.serveErr; !isUseOfClosedError(err) { | |
| 		return err | |
| 	} | |
| 	return nil | |
| } | |
| 
 | |
| func (s *server) Stop() error { | |
| 	s.stopOnce.Do(func() { | |
| 		defer stats.BumpTime(s.stats, "stop.time").End() | |
| 		stats.BumpSum(s.stats, "stop", 1) | |
| 
 | |
| 		// first disable keep-alive for new connections | |
| 		s.server.SetKeepAlivesEnabled(false) | |
| 
 | |
| 		// then close the listener so new connections can't connect come thru | |
| 		closeErr := s.listener.Close() | |
| 		<-s.serveDone | |
| 
 | |
| 		// then trigger the background goroutine to stop and wait for it | |
| 		stopDone := make(chan struct{}) | |
| 		s.stop <- stopDone | |
| 
 | |
| 		// wait for stop | |
| 		select { | |
| 		case <-stopDone: | |
| 		case <-s.clock.After(s.stopTimeout): | |
| 			defer stats.BumpTime(s.stats, "kill.time").End() | |
| 			stats.BumpSum(s.stats, "kill", 1) | |
| 
 | |
| 			// stop timed out, wait for kill | |
| 			killDone := make(chan struct{}) | |
| 			s.kill <- killDone | |
| 			select { | |
| 			case <-killDone: | |
| 			case <-s.clock.After(s.killTimeout): | |
| 				// kill timed out, give up | |
| 				stats.BumpSum(s.stats, "kill.timeout", 1) | |
| 			} | |
| 		} | |
| 
 | |
| 		if closeErr != nil && !isUseOfClosedError(closeErr) { | |
| 			stats.BumpSum(s.stats, "listener.close.error", 1) | |
| 			s.stopErr = closeErr | |
| 		} | |
| 	}) | |
| 	return s.stopErr | |
| } | |
| 
 | |
| func isUseOfClosedError(err error) bool { | |
| 	if err == nil { | |
| 		return false | |
| 	} | |
| 	if opErr, ok := err.(*net.OpError); ok { | |
| 		err = opErr.Err | |
| 	} | |
| 	return err.Error() == "use of closed network connection" | |
| } | |
| 
 | |
| // ListenAndServe is a convenience function to serve and wait for a SIGTERM | |
| // or SIGINT before shutting down. | |
| func ListenAndServe(s *http.Server, hd *HTTP) error { | |
| 	if hd == nil { | |
| 		hd = &HTTP{} | |
| 	} | |
| 	hs, err := hd.ListenAndServe(s) | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	waiterr := make(chan error, 1) | |
| 	go func() { | |
| 		defer close(waiterr) | |
| 		waiterr <- hs.Wait() | |
| 	}() | |
| 
 | |
| 	signals := make(chan os.Signal, 10) | |
| 	signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) | |
| 
 | |
| 	select { | |
| 	case err := <-waiterr: | |
| 		if err != nil { | |
| 			return err | |
| 		} | |
| 	case <-signals: | |
| 		signal.Stop(signals) | |
| 		if err := hs.Stop(); err != nil { | |
| 			return err | |
| 		} | |
| 		if err := <-waiterr; err != nil { | |
| 			return err | |
| 		} | |
| 	} | |
| 	return nil | |
| }
 |