Last active
March 1, 2023 16:06
-
-
Save rcrowley/5474430 to your computer and use it in GitHub Desktop.
Graceful stop in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
Updated to address this after seeing my name in IRC. Thanks and sorry for propagating this bug so far.
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.)
Looks like a simpler solution: https://talks.golang.org/2013/bestpractices.slide#28
Thanks this helped me a lot.
... Or just use Listener.Close()
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
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