Skip to content

Instantly share code, notes, and snippets.

@rcrowley
Last active March 1, 2023 16:06
Show Gist options
  • Save rcrowley/5474430 to your computer and use it in GitHub Desktop.
Save rcrowley/5474430 to your computer and use it in GitHub Desktop.
Graceful stop in Go
package main
import (
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// An uninteresting service.
type Service struct {
ch chan bool
waitGroup *sync.WaitGroup
}
// Make a new Service.
func NewService() *Service {
s := &Service{
ch: make(chan bool),
waitGroup: &sync.WaitGroup{},
}
s.waitGroup.Add(1)
return s
}
// Accept connections and spawn a goroutine to serve each one. Stop listening
// if anything is received on the service's channel.
func (s *Service) Serve(listener *net.TCPListener) {
defer s.waitGroup.Done()
for {
select {
case <-s.ch:
log.Println("stopping listening on", listener.Addr())
listener.Close()
return
default:
}
listener.SetDeadline(time.Now().Add(1e9))
conn, err := listener.AcceptTCP()
if nil != err {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
continue
}
log.Println(err)
}
log.Println(conn.RemoteAddr(), "connected")
s.waitGroup.Add(1)
go s.serve(conn)
}
}
// Stop the service by closing the service's channel. Block until the service
// is really stopped.
func (s *Service) Stop() {
close(s.ch)
s.waitGroup.Wait()
}
// Serve a connection by reading and writing what was read. That's right, this
// is an echo service. Stop reading and writing if anything is received on the
// service's channel but only after writing what was read.
func (s *Service) serve(conn *net.TCPConn) {
defer conn.Close()
defer s.waitGroup.Done()
for {
select {
case <-s.ch:
log.Println("disconnecting", conn.RemoteAddr())
return
default:
}
conn.SetDeadline(time.Now().Add(1e9))
buf := make([]byte, 4096)
if _, err := conn.Read(buf); nil != err {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
continue
}
log.Println(err)
return
}
if _, err := conn.Write(buf); nil != err {
log.Println(err)
return
}
}
}
func main() {
// Listen on 127.0.0.1:48879. That's my favorite port number because in
// hex 48879 is 0xBEEF.
laddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:48879")
if nil != err {
log.Fatalln(err)
}
listener, err := net.ListenTCP("tcp", laddr)
if nil != err {
log.Fatalln(err)
}
log.Println("listening on", listener.Addr())
// Make a new service and send it into the background.
service := NewService()
go service.Serve(listener)
// Handle SIGINT and SIGTERM.
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
// Stop the service gracefully.
service.Stop()
}
@awly
Copy link

awly commented May 1, 2013

Hi, I believe you have a synchronization bug in this code:
sync.WaitGroup.Add should be called before starting the goroutine it is related to, not in the goroutine itself.
The bug will create a possibility of loosing some connection(s) while stopping the server.
The chance of that happening is extremely low, though.
Please, take a look at example sync.WaitGroup usage in sync package: http://golang.org/pkg/sync/#example_WaitGroup
and sync.WaitGroup docs: " The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished"
-Andrew

@rcrowley
Copy link
Author

Updated to address this after seeing my name in IRC. Thanks and sorry for propagating this bug so far.

@rnapier
Copy link

rnapier commented Apr 27, 2014

Thanks for the ideas. Rather than polling on s, could the service just call Close() on the net.listener to mean "stop?" (In the current code, the Service doesn't keep track of the listener, but it could easily store that.)

@mgenov
Copy link

mgenov commented Mar 15, 2015

@luiscarrasco
Copy link

Thanks this helped me a lot.

@emersion
Copy link

... Or just use Listener.Close()

@SavostinVladimir
Copy link

Hi! Thanks a lot! Is there way to gracefully stop read from connection?

This is my solution, but I receive err:="read tcp ip1->ip2: use of closed network connection":

func (m *Proto) readRoutine(conn *net.TCPConn, stopRead chan bool) {
	//start to read in goroutine and send data to the chan
	data := make(chan interface{})
	defer close(data)
	go func() {
		for {
			d, err := conn.read()
                        if err != nil {
				fmt.Println(err)
			}
			if d == nil {
				return
			}
			data <- d
		}
	}()

	//listen stop and data channels and do stuff
	for {
		select {
		case <-stopRead:
			return
		case d := <-data:
			process( d)
		}
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment