Refactoring: Progress.Stop() to Progress.Wait()
Vladimir Bauer
8 years ago
| 63 | 63 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) |
| 64 | 64 | bar.Increment() |
| 65 | 65 | } |
| 66 | // Gracefully shutdown mpb's monitor goroutine | |
| 67 | p.Stop() | |
| 66 | // Wait for all bars to complete | |
| 67 | p.Wait() | |
| 68 | 68 | ``` |
| 69 | 69 | |
| 70 | 70 | #### [Rendering multiple bars](examples/simple/main.go) |
| 98 | 98 | } |
| 99 | 99 | }() |
| 100 | 100 | } |
| 101 | // Gracefully shutdown mpb's monitor goroutine | |
| 102 | p.Stop() | |
| 101 | // Wait for all bars to complete | |
| 102 | p.Wait() | |
| 103 | 103 | ``` |
| 104 | 104 | |
| 105 | 105 | #### [Dynamic Total](examples/dynTotal/main.go) |
| 4 | 4 | "fmt" |
| 5 | 5 | "io" |
| 6 | 6 | "strings" |
| 7 | "sync" | |
| 8 | 7 | "time" |
| 9 | 8 | "unicode/utf8" |
| 10 | 9 | |
| 38 | 37 | done chan struct{} |
| 39 | 38 | shutdown chan struct{} |
| 40 | 39 | |
| 41 | // cacheState is used after done is closed | |
| 40 | // it's guaranted that cacheState isn't nil, after done channel is closed | |
| 42 | 41 | cacheState *bState |
| 43 | 42 | } |
| 44 | 43 | |
| 78 | 77 | } |
| 79 | 78 | ) |
| 80 | 79 | |
| 81 | func newBar(id int, total int64, wg *sync.WaitGroup, cancel <-chan struct{}, options ...BarOption) *Bar { | |
| 80 | func newBar(id int, total int64, cancel <-chan struct{}, options ...BarOption) *Bar { | |
| 82 | 81 | if total <= 0 { |
| 83 | 82 | total = time.Now().Unix() |
| 84 | 83 | } |
| 104 | 103 | shutdown: make(chan struct{}), |
| 105 | 104 | } |
| 106 | 105 | |
| 107 | go b.serve(s, wg, cancel) | |
| 106 | go b.serve(s, cancel) | |
| 108 | 107 | return b |
| 109 | 108 | } |
| 110 | 109 | |
| 289 | 288 | } |
| 290 | 289 | } |
| 291 | 290 | |
| 292 | func (b *Bar) serve(s *bState, wg *sync.WaitGroup, cancel <-chan struct{}) { | |
| 291 | func (b *Bar) serve(s *bState, cancel <-chan struct{}) { | |
| 293 | 292 | for { |
| 294 | 293 | select { |
| 295 | 294 | case op := <-b.operateState: |
| 300 | 299 | case <-b.shutdown: |
| 301 | 300 | b.cacheState = s |
| 302 | 301 | close(b.done) |
| 303 | wg.Done() | |
| 304 | 302 | return |
| 305 | 303 | } |
| 306 | 304 | } |
| 36 | 36 | bar.Increment() |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | p.Stop() | |
| 39 | p.Wait() | |
| 40 | 40 | |
| 41 | 41 | gotWidth := utf8.RuneCount(buf.Bytes()) |
| 42 | 42 | if gotWidth != tc.expected { |
| 59 | 59 | count++ |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | p.Stop() | |
| 62 | p.Wait() | |
| 63 | 63 | if count != total { |
| 64 | 64 | t.Errorf("got count: %d, expected %d\n", count, total) |
| 65 | 65 | } |
| 80 | 80 | }(bars[i]) |
| 81 | 81 | } |
| 82 | 82 | |
| 83 | p.Stop() | |
| 83 | p.Wait() | |
| 84 | 84 | for wantID, bar := range bars { |
| 85 | 85 | gotID := bar.ID() |
| 86 | 86 | if gotID != wantID { |
| 111 | 111 | time.Sleep(10 * time.Millisecond) |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | p.Stop() | |
| 114 | p.Wait() | |
| 115 | 115 | |
| 116 | 116 | wantBar := fmt.Sprintf("[%s%s]", |
| 117 | 117 | strings.Repeat(string(refillChar), till-1), |
| 151 | 151 | }() |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | p.Stop() | |
| 154 | p.Wait() | |
| 155 | 155 | |
| 156 | 156 | wantPanic = fmt.Sprintf("b#%02d panic: %v", 2, wantPanic) |
| 157 | 157 | |
| 10 | 10 | for i := 0; i < total; i++ { |
| 11 | 11 | bar.Increment() |
| 12 | 12 | } |
| 13 | p.Stop() | |
| 13 | p.Wait() | |
| 14 | 14 | } |
| 15 | 15 | |
| 16 | 16 | func BenchmarkSingleBar100(b *testing.B) { |
| 37 | 37 | for i := 0; i < b.N; i++ { |
| 38 | 38 | bar.Increment() |
| 39 | 39 | } |
| 40 | p.Stop() | |
| 40 | p.Wait() | |
| 41 | 41 | } |
| 42 | 42 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) |
| 43 | 43 | bar.Increment() |
| 44 | 44 | } |
| 45 | ||
| 46 | p.Stop() | |
| 45 | // Wait for all bars to complete | |
| 46 | p.Wait() | |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | func ExampleBar_Completed() { |
| 56 | 56 | bar.Increment() |
| 57 | 57 | } |
| 58 | 58 | |
| 59 | p.Stop() | |
| 59 | p.Wait() | |
| 60 | 60 | } |
| 27 | 27 | go download(&wg, p, name, url, i) |
| 28 | 28 | } |
| 29 | 29 | |
| 30 | p.Stop() | |
| 31 | fmt.Println("Finished") | |
| 30 | p.Wait() | |
| 31 | fmt.Println("done") | |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | func download(wg *sync.WaitGroup, p *mpb.Progress, name, url string, n int) { |
| 48 | 48 | // and copy from reader, ignoring errors |
| 49 | 49 | io.Copy(dest, reader) |
| 50 | 50 | |
| 51 | p.Stop() // if you omit this line, rendering bars goroutine will quit early | |
| 52 | fmt.Println("Finished") | |
| 51 | p.Wait() // if you omit this line, rendering bars goroutine will quit early | |
| 52 | fmt.Println("done") | |
| 53 | 53 | } |
| 43 | 43 | } |
| 44 | 44 | }() |
| 45 | 45 | } |
| 46 | // Gracefully shutdown mpb's monitor goroutine | |
| 47 | p.Stop() | |
| 46 | // Wait for all bars to complete | |
| 47 | p.Wait() | |
| 48 | 48 | } |
| 42 | 42 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) |
| 43 | 43 | bar.Increment() |
| 44 | 44 | } |
| 45 | // Gracefully shutdown mpb's monitor goroutine | |
| 46 | p.Stop() | |
| 45 | // Wait for all bars to complete | |
| 46 | p.Wait() | |
| 47 | 47 | } |
| 14 | 14 | type ProgressOption func(*pState) |
| 15 | 15 | |
| 16 | 16 | // WithWaitGroup provides means to have a single joint point. |
| 17 | // If *sync.WaitGroup is provided, you can safely call just p.Stop() | |
| 17 | // If *sync.WaitGroup is provided, you can safely call just p.Wait() | |
| 18 | 18 | // without calling Wait() on provided *sync.WaitGroup. |
| 19 | 19 | // Makes sense when there are more than one bar to render. |
| 20 | 20 | func WithWaitGroup(wg *sync.WaitGroup) ProgressOption { |
| 21 | 21 | return func(s *pState) { |
| 22 | s.ewg = wg | |
| 22 | s.uwg = wg | |
| 23 | 23 | } |
| 24 | 24 | } |
| 25 | 25 | |
| 61 | 61 | } |
| 62 | 62 | } |
| 63 | 63 | |
| 64 | // WithShutdownNotifier provided chanel will be closed, inside p.Stop() call | |
| 64 | // WithShutdownNotifier provided chanel will be closed, after all bars have been rendered. | |
| 65 | 65 | func WithShutdownNotifier(ch chan struct{}) ProgressOption { |
| 66 | 66 | return func(s *pState) { |
| 67 | 67 | s.shutdownNotifier = ch |
| 20 | 20 | |
| 21 | 21 | // Progress represents the container that renders Progress bars |
| 22 | 22 | type Progress struct { |
| 23 | // wg for internal rendering sync | |
| 24 | wg *sync.WaitGroup | |
| 25 | // External wg | |
| 26 | ewg *sync.WaitGroup | |
| 27 | ||
| 28 | 23 | operateState chan func(*pState) |
| 29 | 24 | done chan struct{} |
| 30 | shutdown chan struct{} | |
| 31 | once sync.Once | |
| 32 | ||
| 33 | cacheHeap *priorityQueue | |
| 34 | 25 | } |
| 35 | 26 | |
| 36 | 27 | type ( |
| 37 | 28 | // progress state, which may contain several bars |
| 38 | 29 | pState struct { |
| 39 | bHeap *priorityQueue | |
| 40 | heapUpdated bool | |
| 41 | idCounter int | |
| 42 | width int | |
| 43 | format string | |
| 44 | rr time.Duration | |
| 45 | ewg *sync.WaitGroup | |
| 46 | cw *cwriter.Writer | |
| 47 | ticker *time.Ticker | |
| 48 | interceptors []func(io.Writer) | |
| 49 | ||
| 30 | bHeap *priorityQueue | |
| 31 | heapUpdated bool | |
| 32 | idCounter int | |
| 33 | width int | |
| 34 | format string | |
| 35 | rr time.Duration | |
| 36 | cw *cwriter.Writer | |
| 37 | ticker *time.Ticker | |
| 38 | ||
| 39 | // following are provided by user | |
| 40 | uwg *sync.WaitGroup | |
| 41 | cancel <-chan struct{} | |
| 50 | 42 | shutdownNotifier chan struct{} |
| 51 | cancel <-chan struct{} | |
| 43 | interceptors []func(io.Writer) | |
| 52 | 44 | } |
| 53 | 45 | widthSyncer struct { |
| 54 | 46 | // Public for easy testing |
| 80 | 72 | } |
| 81 | 73 | |
| 82 | 74 | p := &Progress{ |
| 83 | ewg: s.ewg, | |
| 84 | wg: new(sync.WaitGroup), | |
| 85 | 75 | operateState: make(chan func(*pState)), |
| 86 | 76 | done: make(chan struct{}), |
| 87 | shutdown: make(chan struct{}), | |
| 88 | 77 | } |
| 89 | 78 | go p.serve(s) |
| 90 | 79 | return p |
| 92 | 81 | |
| 93 | 82 | // AddBar creates a new progress bar and adds to the container. |
| 94 | 83 | func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { |
| 95 | p.wg.Add(1) | |
| 96 | 84 | result := make(chan *Bar, 1) |
| 97 | 85 | select { |
| 98 | 86 | case p.operateState <- func(s *pState) { |
| 99 | 87 | options = append(options, barWidth(s.width), barFormat(s.format)) |
| 100 | b := newBar(s.idCounter, total, p.wg, s.cancel, options...) | |
| 88 | b := newBar(s.idCounter, total, s.cancel, options...) | |
| 101 | 89 | heap.Push(s.bHeap, b) |
| 102 | 90 | s.heapUpdated = true |
| 103 | 91 | s.idCounter++ |
| 135 | 123 | }: |
| 136 | 124 | return <-result |
| 137 | 125 | case <-p.done: |
| 138 | return p.cacheHeap.Len() | |
| 139 | } | |
| 140 | } | |
| 141 | ||
| 142 | // Stop is a way to gracefully shutdown mpb's rendering goroutine. | |
| 143 | // It is NOT for cancellation (use mpb.WithContext for cancellation purposes). | |
| 144 | // If *sync.WaitGroup has been provided via mpb.WithWaitGroup(), its Wait() | |
| 145 | // method will be called first. | |
| 126 | return 0 | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | // Wait first waits for all bars to complete, then waits for user provided WaitGroup, if any. | |
| 131 | // It's optional to call, in other words if you don't call Progress.Wait(), | |
| 132 | // it's not guaranted that all bars will be flushed completely to the underlying io.Writer. | |
| 133 | func (p *Progress) Wait() { | |
| 134 | <-p.done | |
| 135 | } | |
| 136 | ||
| 137 | // Stop deprecated, use Progress.Wait instead. | |
| 146 | 138 | func (p *Progress) Stop() { |
| 147 | if p.ewg != nil { | |
| 148 | p.ewg.Wait() | |
| 149 | } | |
| 150 | // first wait for all bars to quit | |
| 151 | p.wg.Wait() | |
| 152 | p.once.Do(func() { | |
| 153 | close(p.shutdown) | |
| 154 | }) | |
| 155 | <-p.done | |
| 139 | p.Wait() | |
| 156 | 140 | } |
| 157 | 141 | |
| 158 | 142 | func newWidthSyncer(timeout <-chan struct{}, numBars, numColumn int) *widthSyncer { |
| 204 | 188 | r := <-br.ready |
| 205 | 189 | _, err = s.cw.ReadFrom(r) |
| 206 | 190 | if !br.bar.completed && r.toComplete { |
| 191 | close(br.bar.shutdown) | |
| 207 | 192 | br.bar.completed = true |
| 208 | close(br.bar.shutdown) | |
| 209 | 193 | } |
| 210 | 194 | if r.toRemove { |
| 211 | 195 | s.heapUpdated = heap.Remove(s.bHeap, br.bar.index) != nil |
| 235 | 219 | return slice |
| 236 | 220 | } |
| 237 | 221 | |
| 222 | func (s *pState) waitAll() { | |
| 223 | for s.bHeap.Len() > 0 { | |
| 224 | b := heap.Pop(s.bHeap).(*Bar) | |
| 225 | <-b.done | |
| 226 | } | |
| 227 | if s.uwg != nil { | |
| 228 | s.uwg.Wait() | |
| 229 | } | |
| 230 | } | |
| 231 | ||
| 238 | 232 | func calcMax(slice []int) int { |
| 239 | 233 | if len(slice) == 0 { |
| 240 | 234 | return 0 |
| 34 | 34 | |
| 35 | 35 | time.AfterFunc(100*time.Millisecond, cancel) |
| 36 | 36 | |
| 37 | p.Stop() | |
| 37 | p.Wait() | |
| 38 | 38 | for _, bar := range bars { |
| 39 | 39 | if bar.Current() >= bar.Total() { |
| 40 | 40 | t.Errorf("bar %d: total = %d, current = %d\n", bar.ID(), bar.Total(), bar.Current()) |
| 40 | 40 | if err != nil { |
| 41 | 41 | fmt.Fprintln(os.Stderr, err) |
| 42 | 42 | } |
| 43 | var completed int | |
| 44 | for i := 0; i < s.bHeap.Len(); i++ { | |
| 45 | b := (*s.bHeap)[i] | |
| 46 | if b.completed { | |
| 47 | completed++ | |
| 48 | } | |
| 49 | } | |
| 50 | if completed == s.bHeap.Len() { | |
| 51 | s.ticker.Stop() | |
| 52 | signal.Stop(winch) | |
| 53 | s.waitAll() | |
| 54 | if s.shutdownNotifier != nil { | |
| 55 | close(s.shutdownNotifier) | |
| 56 | } | |
| 57 | close(p.done) | |
| 58 | return | |
| 59 | } | |
| 43 | 60 | case <-winch: |
| 44 | 61 | tw, _, _ := cwriter.TermSize() |
| 45 | 62 | err := s.writeAndFlush(tw-tw/8, numP, numA) |
| 55 | 72 | case <-resumeTicker: |
| 56 | 73 | s.ticker = time.NewTicker(s.rr) |
| 57 | 74 | resumeTicker = nil |
| 58 | case <-p.shutdown: | |
| 59 | s.ticker.Stop() | |
| 60 | signal.Stop(winch) | |
| 61 | p.cacheHeap = s.bHeap | |
| 62 | close(p.done) | |
| 63 | if s.shutdownNotifier != nil { | |
| 64 | close(s.shutdownNotifier) | |
| 65 | } | |
| 66 | return | |
| 67 | 75 | } |
| 68 | 76 | } |
| 69 | 77 | } |
| 39 | 39 | } |
| 40 | 40 | |
| 41 | 41 | b.Complete() |
| 42 | p.Stop() | |
| 42 | p.Wait() | |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | 45 | func TestRemoveBars(t *testing.T) { |
| 71 | 71 | } |
| 72 | 72 | }() |
| 73 | 73 | } |
| 74 | p.Stop() | |
| 74 | p.Wait() | |
| 75 | 75 | } |
| 76 | 76 | |
| 77 | 77 | func TestWithCancel(t *testing.T) { |
| 100 | 100 | close(cancel) |
| 101 | 101 | }) |
| 102 | 102 | |
| 103 | p.Stop() | |
| 103 | p.Wait() | |
| 104 | 104 | for _, bar := range bars { |
| 105 | 105 | if bar.Current() >= bar.Total() { |
| 106 | 106 | t.Errorf("bar %d: total = %d, current = %d\n", bar.ID(), bar.Total(), bar.Current()) |
| 138 | 138 | |
| 139 | 139 | wg.Wait() |
| 140 | 140 | close(cancel) |
| 141 | p.Stop() | |
| 141 | p.Wait() | |
| 142 | 142 | |
| 143 | 143 | for _, r := range customFormat { |
| 144 | 144 | if !bytes.ContainsRune(buf.Bytes(), r) { |
| 163 | 163 | bar.Increment() |
| 164 | 164 | } |
| 165 | 165 | |
| 166 | p.Stop() | |
| 166 | p.Wait() | |
| 167 | 167 | |
| 168 | 168 | got := buf.String() |
| 169 | 169 | want := fmt.Sprintf("[%s]", strings.Repeat("=", customWidth-2)) |
| 30 | 30 | if err != nil { |
| 31 | 31 | fmt.Fprintln(os.Stderr, err) |
| 32 | 32 | } |
| 33 | case <-p.shutdown: | |
| 34 | s.ticker.Stop() | |
| 35 | p.cacheHeap = s.bHeap | |
| 36 | close(p.done) | |
| 37 | if s.shutdownNotifier != nil { | |
| 38 | close(s.shutdownNotifier) | |
| 33 | var completed int | |
| 34 | for i := 0; i < s.bHeap.Len(); i++ { | |
| 35 | b := (*s.bHeap)[i] | |
| 36 | if b.completed { | |
| 37 | completed++ | |
| 38 | } | |
| 39 | 39 | } |
| 40 | return | |
| 40 | if completed == s.bHeap.Len() { | |
| 41 | s.ticker.Stop() | |
| 42 | s.waitAll() | |
| 43 | if s.shutdownNotifier != nil { | |
| 44 | close(s.shutdownNotifier) | |
| 45 | } | |
| 46 | close(p.done) | |
| 47 | return | |
| 48 | } | |
| 41 | 49 | } |
| 42 | 50 | } |
| 43 | 51 | } |