Browse code

Compare event nanoseconds properly to filter since a specific date.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2016/03/08 09:02:35
Showing 5 changed files
... ...
@@ -50,33 +50,23 @@ func (e *Events) Subscribe() ([]eventtypes.Message, chan interface{}, func()) {
50 50
 // of interface{}, so you need type assertion).
51 51
 func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]eventtypes.Message, chan interface{}) {
52 52
 	e.mu.Lock()
53
-	defer e.mu.Unlock()
54 53
 
55
-	var buffered []eventtypes.Message
56
-	topic := func(m interface{}) bool {
57
-		return ef.Include(m.(eventtypes.Message))
54
+	var topic func(m interface{}) bool
55
+	if ef != nil && ef.filter.Len() > 0 {
56
+		topic = func(m interface{}) bool { return ef.Include(m.(eventtypes.Message)) }
58 57
 	}
59 58
 
60
-	if since != -1 {
61
-		for i := len(e.events) - 1; i >= 0; i-- {
62
-			ev := e.events[i]
63
-			if ev.Time < since || ((ev.Time == since) && (ev.TimeNano < sinceNano)) {
64
-				break
65
-			}
66
-			if ef.filter.Len() == 0 || topic(ev) {
67
-				buffered = append([]eventtypes.Message{ev}, buffered...)
68
-			}
69
-		}
70
-	}
59
+	buffered := e.loadBufferedEvents(since, sinceNano, topic)
71 60
 
72 61
 	var ch chan interface{}
73
-	if ef.filter.Len() > 0 {
62
+	if topic != nil {
74 63
 		ch = e.pub.SubscribeTopic(topic)
75 64
 	} else {
76 65
 		// Subscribe to all events if there are no filters
77 66
 		ch = e.pub.Subscribe()
78 67
 	}
79 68
 
69
+	e.mu.Unlock()
80 70
 	return buffered, ch
81 71
 }
82 72
 
... ...
@@ -124,3 +114,29 @@ func (e *Events) Log(action, eventType string, actor eventtypes.Actor) {
124 124
 func (e *Events) SubscribersCount() int {
125 125
 	return e.pub.Len()
126 126
 }
127
+
128
+// loadBufferedEvents iterates over the cached events in the buffer
129
+// and returns those that were emitted before a specific date.
130
+// The date is splitted in two values:
131
+//   - the `since` argument is a date timestamp without nanoseconds, or -1 to return an empty slice.
132
+//   - the `sinceNano` argument is the nanoseconds offset from the timestamp.
133
+// It uses `time.Unix(seconds, nanoseconds)` to generate a valid date with those two first arguments.
134
+// It filters those buffered messages with a topic function if it's not nil, otherwise it adds all messages.
135
+func (e *Events) loadBufferedEvents(since, sinceNano int64, topic func(interface{}) bool) []eventtypes.Message {
136
+	var buffered []eventtypes.Message
137
+	if since == -1 {
138
+		return buffered
139
+	}
140
+
141
+	sinceNanoUnix := time.Unix(since, sinceNano).UnixNano()
142
+	for i := len(e.events) - 1; i >= 0; i-- {
143
+		ev := e.events[i]
144
+		if ev.TimeNano < sinceNanoUnix {
145
+			break
146
+		}
147
+		if topic == nil || topic(ev) {
148
+			buffered = append([]eventtypes.Message{ev}, buffered...)
149
+		}
150
+	}
151
+	return buffered
152
+}
... ...
@@ -5,7 +5,9 @@ import (
5 5
 	"testing"
6 6
 	"time"
7 7
 
8
+	"github.com/docker/docker/daemon/events/testutils"
8 9
 	"github.com/docker/engine-api/types/events"
10
+	timetypes "github.com/docker/engine-api/types/time"
9 11
 )
10 12
 
11 13
 func TestEventsLog(t *testing.T) {
... ...
@@ -150,3 +152,45 @@ func TestLogEvents(t *testing.T) {
150 150
 		t.Fatalf("Last action is %s, must be action_89", lastC.Status)
151 151
 	}
152 152
 }
153
+
154
+// https://github.com/docker/docker/issues/20999
155
+// Fixtures:
156
+//
157
+//2016-03-07T17:28:03.022433271+02:00 container die 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)
158
+//2016-03-07T17:28:03.091719377+02:00 network disconnect 19c5ed41acb798f26b751e0035cd7821741ab79e2bbd59a66b5fd8abf954eaa0 (type=bridge, container=0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079, name=bridge)
159
+//2016-03-07T17:28:03.129014751+02:00 container destroy 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)
160
+func TestLoadBufferedEvents(t *testing.T) {
161
+	now := time.Now()
162
+	f, err := timetypes.GetTimestamp("2016-03-07T17:28:03.100000000+02:00", now)
163
+	if err != nil {
164
+		t.Fatal(err)
165
+	}
166
+	since, sinceNano, err := timetypes.ParseTimestamps(f, -1)
167
+	if err != nil {
168
+		t.Fatal(err)
169
+	}
170
+
171
+	m1, err := eventstestutils.Scan("2016-03-07T17:28:03.022433271+02:00 container die 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)")
172
+	if err != nil {
173
+		t.Fatal(err)
174
+	}
175
+	m2, err := eventstestutils.Scan("2016-03-07T17:28:03.091719377+02:00 network disconnect 19c5ed41acb798f26b751e0035cd7821741ab79e2bbd59a66b5fd8abf954eaa0 (type=bridge, container=0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079, name=bridge)")
176
+	if err != nil {
177
+		t.Fatal(err)
178
+	}
179
+	m3, err := eventstestutils.Scan("2016-03-07T17:28:03.129014751+02:00 container destroy 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)")
180
+	if err != nil {
181
+		t.Fatal(err)
182
+	}
183
+
184
+	buffered := []events.Message{*m1, *m2, *m3}
185
+
186
+	events := &Events{
187
+		events: buffered,
188
+	}
189
+
190
+	out := events.loadBufferedEvents(since, sinceNano, nil)
191
+	if len(out) != 1 {
192
+		t.Fatalf("expected 1 message, got %d: %v", len(out), out)
193
+	}
194
+}
153 195
new file mode 100644
... ...
@@ -0,0 +1,76 @@
0
+package eventstestutils
1
+
2
+import (
3
+	"fmt"
4
+	"regexp"
5
+	"strings"
6
+	"time"
7
+
8
+	"github.com/docker/engine-api/types/events"
9
+	timetypes "github.com/docker/engine-api/types/time"
10
+)
11
+
12
+var (
13
+	reTimestamp  = `(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}(:?(:?(:?-|\+)\d{2}:\d{2})|Z))`
14
+	reEventType  = `(?P<eventType>\w+)`
15
+	reAction     = `(?P<action>\w+)`
16
+	reID         = `(?P<id>[^\s]+)`
17
+	reAttributes = `(\s\((?P<attributes>[^\)]+)\))?`
18
+	reString     = fmt.Sprintf(`\A%s\s%s\s%s\s%s%s\z`, reTimestamp, reEventType, reAction, reID, reAttributes)
19
+
20
+	// eventCliRegexp is a regular expression that matches all possible event outputs in the cli
21
+	eventCliRegexp = regexp.MustCompile(reString)
22
+)
23
+
24
+// ScanMap turns an event string like the default ones formatted in the cli output
25
+// and turns it into map.
26
+func ScanMap(text string) map[string]string {
27
+	matches := eventCliRegexp.FindAllStringSubmatch(text, -1)
28
+	md := map[string]string{}
29
+	if len(matches) == 0 {
30
+		return md
31
+	}
32
+
33
+	names := eventCliRegexp.SubexpNames()
34
+	for i, n := range matches[0] {
35
+		md[names[i]] = n
36
+	}
37
+	return md
38
+}
39
+
40
+// Scan turns an event string like the default ones formatted in the cli output
41
+// and turns it into an event message.
42
+func Scan(text string) (*events.Message, error) {
43
+	md := ScanMap(text)
44
+	if len(md) == 0 {
45
+		return nil, fmt.Errorf("text is not an event: %s", text)
46
+	}
47
+
48
+	f, err := timetypes.GetTimestamp(md["timestamp"], time.Now())
49
+	if err != nil {
50
+		return nil, err
51
+	}
52
+
53
+	t, tn, err := timetypes.ParseTimestamps(f, -1)
54
+	if err != nil {
55
+		return nil, err
56
+	}
57
+
58
+	attrs := make(map[string]string)
59
+	for _, a := range strings.SplitN(md["attributes"], ", ", -1) {
60
+		kv := strings.SplitN(a, "=", 2)
61
+		attrs[kv[0]] = kv[1]
62
+	}
63
+
64
+	tu := time.Unix(t, tn)
65
+	return &events.Message{
66
+		Time:     t,
67
+		TimeNano: tu.UnixNano(),
68
+		Type:     md["eventType"],
69
+		Action:   md["action"],
70
+		Actor: events.Actor{
71
+			ID:         md["id"],
72
+			Attributes: attrs,
73
+		},
74
+	}, nil
75
+}
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"sync"
13 13
 	"time"
14 14
 
15
+	"github.com/docker/docker/daemon/events/testutils"
15 16
 	"github.com/docker/docker/pkg/integration/checker"
16 17
 	"github.com/go-check/check"
17 18
 )
... ...
@@ -152,7 +153,7 @@ func (s *DockerSuite) TestEventsContainerEventsAttrSort(c *check.C) {
152 152
 	c.Assert(nEvents, checker.GreaterOrEqualThan, 3) //Missing expected event
153 153
 	matchedEvents := 0
154 154
 	for _, event := range events {
155
-		matches := parseEventText(event)
155
+		matches := eventstestutils.ScanMap(event)
156 156
 		if matches["id"] != containerID {
157 157
 			continue
158 158
 		}
... ...
@@ -201,7 +202,7 @@ func (s *DockerSuite) TestEventsImageTag(c *check.C) {
201 201
 	c.Assert(events, checker.HasLen, 1, check.Commentf("was expecting 1 event. out=%s", out))
202 202
 	event := strings.TrimSpace(events[0])
203 203
 
204
-	matches := parseEventText(event)
204
+	matches := eventstestutils.ScanMap(event)
205 205
 	c.Assert(matchEventID(matches, image), checker.True, check.Commentf("matches: %v\nout:\n%s", matches, out))
206 206
 	c.Assert(matches["action"], checker.Equals, "tag")
207 207
 }
... ...
@@ -220,7 +221,7 @@ func (s *DockerSuite) TestEventsImagePull(c *check.C) {
220 220
 
221 221
 	events := strings.Split(strings.TrimSpace(out), "\n")
222 222
 	event := strings.TrimSpace(events[len(events)-1])
223
-	matches := parseEventText(event)
223
+	matches := eventstestutils.ScanMap(event)
224 224
 	c.Assert(matches["id"], checker.Equals, "hello-world:latest")
225 225
 	c.Assert(matches["action"], checker.Equals, "pull")
226 226
 
... ...
@@ -245,7 +246,7 @@ func (s *DockerSuite) TestEventsImageImport(c *check.C) {
245 245
 	out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=import")
246 246
 	events := strings.Split(strings.TrimSpace(out), "\n")
247 247
 	c.Assert(events, checker.HasLen, 1)
248
-	matches := parseEventText(events[0])
248
+	matches := eventstestutils.ScanMap(events[0])
249 249
 	c.Assert(matches["id"], checker.Equals, imageRef, check.Commentf("matches: %v\nout:\n%s\n", matches, out))
250 250
 	c.Assert(matches["action"], checker.Equals, "import", check.Commentf("matches: %v\nout:\n%s\n", matches, out))
251 251
 }
... ...
@@ -370,7 +371,7 @@ func (s *DockerSuite) TestEventsFilterContainer(c *check.C) {
370 370
 			return fmt.Errorf("expected 4 events, got %v", events)
371 371
 		}
372 372
 		for _, event := range events {
373
-			matches := parseEventText(event)
373
+			matches := eventstestutils.ScanMap(event)
374 374
 			if !matchEventID(matches, id) {
375 375
 				return fmt.Errorf("expected event for container id %s: %s - parsed container id: %s", id, event, matches["id"])
376 376
 			}
... ...
@@ -3,7 +3,6 @@ package main
3 3
 import (
4 4
 	"bufio"
5 5
 	"bytes"
6
-	"fmt"
7 6
 	"io"
8 7
 	"os/exec"
9 8
 	"regexp"
... ...
@@ -11,22 +10,11 @@ import (
11 11
 	"strings"
12 12
 
13 13
 	"github.com/Sirupsen/logrus"
14
+	"github.com/docker/docker/daemon/events/testutils"
14 15
 	"github.com/docker/docker/pkg/integration/checker"
15 16
 	"github.com/go-check/check"
16 17
 )
17 18
 
18
-var (
19
-	reTimestamp  = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}(:?(:?(:?-|\+)\d{2}:\d{2})|Z)`
20
-	reEventType  = `(?P<eventType>\w+)`
21
-	reAction     = `(?P<action>\w+)`
22
-	reID         = `(?P<id>[^\s]+)`
23
-	reAttributes = `(\s\((?P<attributes>[^\)]+)\))?`
24
-	reString     = fmt.Sprintf(`\A%s\s%s\s%s\s%s%s\z`, reTimestamp, reEventType, reAction, reID, reAttributes)
25
-
26
-	// eventCliRegexp is a regular expression that matches all possible event outputs in the cli
27
-	eventCliRegexp = regexp.MustCompile(reString)
28
-)
29
-
30 19
 // eventMatcher is a function that tries to match an event input.
31 20
 // It returns true if the event matches and a map with
32 21
 // a set of key/value to identify the match.
... ...
@@ -131,7 +119,7 @@ func (e *eventObserver) CheckEventError(c *check.C, id, event string, match even
131 131
 // It returns an empty map and false if there is no match.
132 132
 func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher {
133 133
 	return func(text string) (map[string]string, bool) {
134
-		matches := parseEventText(text)
134
+		matches := eventstestutils.ScanMap(text)
135 135
 		if len(matches) == 0 {
136 136
 			return matches, false
137 137
 		}
... ...
@@ -154,26 +142,10 @@ func processEventMatch(actions map[string]chan bool) eventMatchProcessor {
154 154
 	}
155 155
 }
156 156
 
157
-// parseEventText parses a line of events coming from the cli and returns
158
-// the matchers in a map.
159
-func parseEventText(text string) map[string]string {
160
-	matches := eventCliRegexp.FindAllStringSubmatch(text, -1)
161
-	md := map[string]string{}
162
-	if len(matches) == 0 {
163
-		return md
164
-	}
165
-
166
-	names := eventCliRegexp.SubexpNames()
167
-	for i, n := range matches[0] {
168
-		md[names[i]] = n
169
-	}
170
-	return md
171
-}
172
-
173 157
 // parseEventAction parses an event text and returns the action.
174 158
 // It fails if the text is not in the event format.
175 159
 func parseEventAction(c *check.C, text string) string {
176
-	matches := parseEventText(text)
160
+	matches := eventstestutils.ScanMap(text)
177 161
 	return matches["action"]
178 162
 }
179 163
 
... ...
@@ -182,7 +154,7 @@ func parseEventAction(c *check.C, text string) string {
182 182
 func eventActionsByIDAndType(c *check.C, events []string, id, eventType string) []string {
183 183
 	var filtered []string
184 184
 	for _, event := range events {
185
-		matches := parseEventText(event)
185
+		matches := eventstestutils.ScanMap(event)
186 186
 		c.Assert(matches, checker.Not(checker.IsNil))
187 187
 		if matchIDAndEventType(matches, id, eventType) {
188 188
 			filtered = append(filtered, matches["action"])
... ...
@@ -214,7 +186,7 @@ func matchEventID(matches map[string]string, id string) bool {
214 214
 func parseEvents(c *check.C, out, match string) {
215 215
 	events := strings.Split(strings.TrimSpace(out), "\n")
216 216
 	for _, event := range events {
217
-		matches := parseEventText(event)
217
+		matches := eventstestutils.ScanMap(event)
218 218
 		matched, err := regexp.MatchString(match, matches["action"])
219 219
 		c.Assert(err, checker.IsNil)
220 220
 		c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"]))
... ...
@@ -224,7 +196,7 @@ func parseEvents(c *check.C, out, match string) {
224 224
 func parseEventsWithID(c *check.C, out, match, id string) {
225 225
 	events := strings.Split(strings.TrimSpace(out), "\n")
226 226
 	for _, event := range events {
227
-		matches := parseEventText(event)
227
+		matches := eventstestutils.ScanMap(event)
228 228
 		c.Assert(matchEventID(matches, id), checker.True)
229 229
 
230 230
 		matched, err := regexp.MatchString(match, matches["action"])