write up stop.Group pattern
This commit is contained in:
parent
837e146b2b
commit
29d5515cd7
1 changed files with 121 additions and 0 deletions
121
stop/readme.md
Normal file
121
stop/readme.md
Normal file
|
@ -0,0 +1,121 @@
|
|||
# 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
|
||||
|
||||
```
|
||||
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()
|
||||
<-s.grp.Ch()
|
||||
err := l.Close()
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
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.listenAndServe(l)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
Loading…
Reference in a new issue