Browse code

Bug fix: stats --no-stream always print zero values

`docker stats --no-stream` always print zero values.

```
$ docker stats --no-stream
CONTAINER CPU % MEM USAGE / LIMIT MEM %
NET I/O BLOCK I/O
7f4ef234ca8c 0.00% 0 B / 0 B 0.00%
0 B / 0 B 0 B / 0 B
f05bd18819aa 0.00% 0 B / 0 B 0.00%
0 B / 0 B 0 B / 0 B

```

This commit will let docker client wait until it gets correct stat
data before print it on screen.

Signed-off-by: Zhang Wei <zhangwei555@huawei.com>

Zhang Wei authored on 2016/03/01 16:09:48
Showing 3 changed files
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"fmt"
5 5
 	"io"
6 6
 	"strings"
7
+	"sync"
7 8
 	"text/tabwriter"
8 9
 	"time"
9 10
 
... ...
@@ -59,6 +60,9 @@ func (cli *DockerCli) CmdStats(args ...string) error {
59 59
 		})
60 60
 	}
61 61
 
62
+	// waitFirst is a WaitGroup to wait first stat data's reach for each container
63
+	waitFirst := &sync.WaitGroup{}
64
+
62 65
 	cStats := stats{}
63 66
 	// getContainerList simulates creation event for all previously existing
64 67
 	// containers (only used when calling `docker stats` without arguments).
... ...
@@ -72,8 +76,10 @@ func (cli *DockerCli) CmdStats(args ...string) error {
72 72
 		}
73 73
 		for _, container := range cs {
74 74
 			s := &containerStats{Name: container.ID[:12]}
75
-			cStats.add(s)
76
-			go s.Collect(cli.client, !*noStream)
75
+			if cStats.add(s) {
76
+				waitFirst.Add(1)
77
+				go s.Collect(cli.client, !*noStream, waitFirst)
78
+			}
77 79
 		}
78 80
 	}
79 81
 
... ...
@@ -87,15 +93,19 @@ func (cli *DockerCli) CmdStats(args ...string) error {
87 87
 		eh.Handle("create", func(e events.Message) {
88 88
 			if *all {
89 89
 				s := &containerStats{Name: e.ID[:12]}
90
-				cStats.add(s)
91
-				go s.Collect(cli.client, !*noStream)
90
+				if cStats.add(s) {
91
+					waitFirst.Add(1)
92
+					go s.Collect(cli.client, !*noStream, waitFirst)
93
+				}
92 94
 			}
93 95
 		})
94 96
 
95 97
 		eh.Handle("start", func(e events.Message) {
96 98
 			s := &containerStats{Name: e.ID[:12]}
97
-			cStats.add(s)
98
-			go s.Collect(cli.client, !*noStream)
99
+			if cStats.add(s) {
100
+				waitFirst.Add(1)
101
+				go s.Collect(cli.client, !*noStream, waitFirst)
102
+			}
99 103
 		})
100 104
 
101 105
 		eh.Handle("die", func(e events.Message) {
... ...
@@ -112,14 +122,16 @@ func (cli *DockerCli) CmdStats(args ...string) error {
112 112
 
113 113
 		// Start a short-lived goroutine to retrieve the initial list of
114 114
 		// containers.
115
-		go getContainerList()
115
+		getContainerList()
116 116
 	} else {
117 117
 		// Artificially send creation events for the containers we were asked to
118 118
 		// monitor (same code path than we use when monitoring all containers).
119 119
 		for _, name := range names {
120 120
 			s := &containerStats{Name: name}
121
-			cStats.add(s)
122
-			go s.Collect(cli.client, !*noStream)
121
+			if cStats.add(s) {
122
+				waitFirst.Add(1)
123
+				go s.Collect(cli.client, !*noStream, waitFirst)
124
+			}
123 125
 		}
124 126
 
125 127
 		// We don't expect any asynchronous errors: closeChan can be closed.
... ...
@@ -143,6 +155,9 @@ func (cli *DockerCli) CmdStats(args ...string) error {
143 143
 		}
144 144
 	}
145 145
 
146
+	// before print to screen, make sure each container get at least one valid stat data
147
+	waitFirst.Wait()
148
+
146 149
 	w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
147 150
 	printHeader := func() {
148 151
 		if !*noStream {
... ...
@@ -33,12 +33,14 @@ type stats struct {
33 33
 	cs []*containerStats
34 34
 }
35 35
 
36
-func (s *stats) add(cs *containerStats) {
36
+func (s *stats) add(cs *containerStats) bool {
37 37
 	s.mu.Lock()
38
+	defer s.mu.Unlock()
38 39
 	if _, exists := s.isKnownContainer(cs.Name); !exists {
39 40
 		s.cs = append(s.cs, cs)
41
+		return true
40 42
 	}
41
-	s.mu.Unlock()
43
+	return false
42 44
 }
43 45
 
44 46
 func (s *stats) remove(id string) {
... ...
@@ -58,7 +60,22 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
58 58
 	return -1, false
59 59
 }
60 60
 
61
-func (s *containerStats) Collect(cli client.APIClient, streamStats bool) {
61
+func (s *containerStats) Collect(cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
62
+	var (
63
+		getFirst       bool
64
+		previousCPU    uint64
65
+		previousSystem uint64
66
+		u              = make(chan error, 1)
67
+	)
68
+
69
+	defer func() {
70
+		// if error happens and we get nothing of stats, release wait group whatever
71
+		if !getFirst {
72
+			getFirst = true
73
+			waitFirst.Done()
74
+		}
75
+	}()
76
+
62 77
 	responseBody, err := cli.ContainerStats(context.Background(), s.Name, streamStats)
63 78
 	if err != nil {
64 79
 		s.mu.Lock()
... ...
@@ -68,12 +85,7 @@ func (s *containerStats) Collect(cli client.APIClient, streamStats bool) {
68 68
 	}
69 69
 	defer responseBody.Close()
70 70
 
71
-	var (
72
-		previousCPU    uint64
73
-		previousSystem uint64
74
-		dec            = json.NewDecoder(responseBody)
75
-		u              = make(chan error, 1)
76
-	)
71
+	dec := json.NewDecoder(responseBody)
77 72
 	go func() {
78 73
 		for {
79 74
 			var v *types.StatsJSON
... ...
@@ -125,6 +137,11 @@ func (s *containerStats) Collect(cli client.APIClient, streamStats bool) {
125 125
 			s.BlockRead = 0
126 126
 			s.BlockWrite = 0
127 127
 			s.mu.Unlock()
128
+			// if this is the first stat you get, release WaitGroup
129
+			if !getFirst {
130
+				getFirst = true
131
+				waitFirst.Done()
132
+			}
128 133
 		case err := <-u:
129 134
 			if err != nil {
130 135
 				s.mu.Lock()
... ...
@@ -132,6 +149,11 @@ func (s *containerStats) Collect(cli client.APIClient, streamStats bool) {
132 132
 				s.mu.Unlock()
133 133
 				return
134 134
 			}
135
+			// if this is the first stat you get, release WaitGroup
136
+			if !getFirst {
137
+				getFirst = true
138
+				waitFirst.Done()
139
+			}
135 140
 		}
136 141
 		if !streamStats {
137 142
 			return
... ...
@@ -75,6 +75,18 @@ func (s *DockerSuite) TestStatsAllRunningNoStream(c *check.C) {
75 75
 	if strings.Contains(out, id3) {
76 76
 		c.Fatalf("Did not expect %s in stats, got %s", id3, out)
77 77
 	}
78
+
79
+	// check output contains real data, but not all zeros
80
+	reg, _ := regexp.Compile("[1-9]+")
81
+	// split output with "\n", outLines[1] is id2's output
82
+	// outLines[2] is id1's output
83
+	outLines := strings.Split(out, "\n")
84
+	// check stat result of id2 contains real data
85
+	realData := reg.Find([]byte(outLines[1][12:]))
86
+	c.Assert(realData, checker.NotNil, check.Commentf("stat result are empty: %s", out))
87
+	// check stat result of id1 contains real data
88
+	realData = reg.Find([]byte(outLines[2][12:]))
89
+	c.Assert(realData, checker.NotNil, check.Commentf("stat result are empty: %s", out))
78 90
 }
79 91
 
80 92
 func (s *DockerSuite) TestStatsAllNoStream(c *check.C) {
... ...
@@ -93,6 +105,17 @@ func (s *DockerSuite) TestStatsAllNoStream(c *check.C) {
93 93
 	if !strings.Contains(out, id1) || !strings.Contains(out, id2) {
94 94
 		c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out)
95 95
 	}
96
+
97
+	// check output contains real data, but not all zeros
98
+	reg, _ := regexp.Compile("[1-9]+")
99
+	// split output with "\n", outLines[1] is id2's output
100
+	outLines := strings.Split(out, "\n")
101
+	// check stat result of id2 contains real data
102
+	realData := reg.Find([]byte(outLines[1][12:]))
103
+	c.Assert(realData, checker.NotNil, check.Commentf("stat result of %s is empty: %s", id2, out))
104
+	// check stat result of id1 contains all zero
105
+	realData = reg.Find([]byte(outLines[2][12:]))
106
+	c.Assert(realData, checker.IsNil, check.Commentf("stat result of %s should be empty : %s", id1, out))
96 107
 }
97 108
 
98 109
 func (s *DockerSuite) TestStatsAllNewContainersAdded(c *check.C) {