131 lines
2.6 KiB
Markdown
131 lines
2.6 KiB
Markdown
# Stop Group
|
|
|
|
A stop group is meant to help cleanly shut down a component that uses multiple goroutines.
|
|
|
|
|
|
## The Problem
|
|
|
|
A shutdown typically works as follows:
|
|
|
|
- a component receives a shutdown signal
|
|
- the component passes the shutdown signal to each outstanding goroutine
|
|
- each goroutine stops whatever it is doing and returns
|
|
- once all the goroutines have returned, the component does any final cleanup and signals that it is done
|
|
|
|
There are several gotchas in this process:
|
|
|
|
- each goroutine must be tracked, so all can be stopped
|
|
- the component should not expose implementation details about how many goroutines it uses and how they are managed
|
|
- goroutines may be blocked by a read on a channel. they need to be unblocked during shutdown
|
|
- goroutines may take a while to finish, and may finish in any order. component shutdown is not complete until all goroutines finish
|
|
- using a channel to send the shutdown signal is complicated (doing things in the correct order, closing an already-closed channel, etc)
|
|
|
|
|
|
## The Solution
|
|
|
|
The solution is a stop group. A stop group is a combination of a `sync.WaitGroup` and a cancelable `context`. Here's how it works:
|
|
|
|
```
|
|
grp := stop.New()
|
|
action := func() { ... }
|
|
```
|
|
|
|
All goroutines are started in the start group.
|
|
|
|
```
|
|
grp.Add(1)
|
|
go func() {
|
|
defer grp.Done()
|
|
action()
|
|
}
|
|
```
|
|
|
|
|
|
Any goroutine that may be blocked by a channel read has a simple way of unblocking on shutdown
|
|
|
|
```
|
|
for {
|
|
select {
|
|
case text := <-actionCh:
|
|
fmt.Printf("Got some text: %s", text)
|
|
case <-grp.Ch():
|
|
return
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
Shutting down synchronously is easy
|
|
|
|
```
|
|
grp.StopAndWait()
|
|
```
|
|
|
|
|
|
## Example
|
|
|
|
### Structure
|
|
|
|
```
|
|
type Server struct {
|
|
grp *stop.Group
|
|
}
|
|
|
|
func NewServer() *Server {
|
|
return &Server{
|
|
grp: stop.New(),
|
|
}
|
|
}
|
|
|
|
func (s *Server) Shutdown() {
|
|
s.grp.StopAndWait()
|
|
}
|
|
|
|
func (s *Server) Start(address string) error {
|
|
l, err := net.Listen(network, address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Println("listening on " + address)
|
|
|
|
s.grp.Add(1)
|
|
go func() {
|
|
defer s.grp.Done()
|
|
for {
|
|
select {
|
|
case <-s.grp.Ch():
|
|
return
|
|
case <-time.Tick(10 * time.Second):
|
|
log.Println("still running")
|
|
}
|
|
}
|
|
}()
|
|
|
|
s.grp.Add(1)
|
|
go func() {
|
|
defer s.grp.Done()
|
|
<-s.grp.Ch()
|
|
err := l.Close()
|
|
if err != nil {
|
|
log.Errorln(err)
|
|
}
|
|
}()
|
|
|
|
// listenAndServe blocks until the server is shut down, just like http.ListenAndServe()
|
|
return s.listenAndServe(l)
|
|
}
|
|
```
|
|
|
|
### Usage
|
|
|
|
```
|
|
s := NewServer()
|
|
log.Println("starting")
|
|
go s.Start("localhost:1234")
|
|
|
|
// ... do some other things here ...
|
|
|
|
log.Println("shutting down")
|
|
s.Shutdown()
|
|
log.Println("shutdown complete")
|
|
```
|