'generic' notifier
Juan Batiz-Benet committed
Jan 26, 2015 at 03:40 UTC
0552f46f45b3ac8304ed925de85f77aaee88e940
2 files changed
+320
thirdparty/notifier/notifier.go
new
+113
@@ -0,0 +1,113 @@
1
+// Package notifier provides a simple notification dispatcher
2
+// meant to be embedded in larger structres who wish to allow
3
+// clients to sign up for event notifications.
4
+package notifier
5
+
6
+import (
7
+ "sync"
8
+)
9
+
10
+// Notifiee is a generic interface. Clients implement
11
+// their own Notifiee interfaces to ensure type-safety
12
+// of notifications:
13
+//
14
+// type RocketNotifiee interface{
15
+// Countdown(r Rocket, countdown time.Duration)
16
+// LiftedOff(Rocket)
17
+// ReachedOrbit(Rocket)
18
+// Detached(Rocket, Capsule)
19
+// Landed(Rocket)
20
+// }
21
+//
22
+type Notifiee interface{}
23
+
24
+// Notifier is a notification dispatcher. It's meant
25
+// to be composed, and its zero-value is ready to be used.
26
+//
27
+// type Rocket struct {
28
+// notifier notifier.Notifier
29
+// }
30
+//
31
+type Notifier struct {
32
+ mu sync.RWMutex // guards notifiees
33
+ nots map[Notifiee]struct{}
34
+}
35
+
36
+// Notify signs up Notifiee e for notifications. This function
37
+// is meant to be called behind your own type-safe function(s):
38
+//
39
+// // generic function for pattern-following
40
+// func (r *Rocket) Notify(n Notifiee) {
41
+// r.notifier.Notify(n)
42
+// }
43
+//
44
+// // or as part of other functions
45
+// func (r *Rocket) Onboard(a Astronaut) {
46
+// r.astronauts = append(r.austronauts, a)
47
+// r.notifier.Notify(a)
48
+// }
49
+//
50
+func (n *Notifier) Notify(e Notifiee) {
51
+ n.mu.Lock()
52
+ if n.nots == nil { // so that zero-value is ready to be used.
53
+ n.nots = make(map[Notifiee]struct{})
54
+ }
55
+ n.nots[e] = struct{}{}
56
+ n.mu.Unlock()
57
+}
58
+
59
+// StopNotifying stops notifying Notifiee e. This function
60
+// is meant to be called behind your own type-safe function(s):
61
+//
62
+// // generic function for pattern-following
63
+// func (r *Rocket) StopNotify(n Notifiee) {
64
+// r.notifier.StopNotify(n)
65
+// }
66
+//
67
+// // or as part of other functions
68
+// func (r *Rocket) Detach(c Capsule) {
69
+// r.notifier.StopNotify(c)
70
+// r.capsule = nil
71
+// }
72
+//
73
+func (n *Notifier) StopNotify(e Notifiee) {
74
+ n.mu.Lock()
75
+ if n.nots != nil { // so that zero-value is ready to be used.
76
+ delete(n.nots, e)
77
+ }
78
+ n.mu.Unlock()
79
+}
80
+
81
+// NotifyAll messages the notifier's notifiees with a given notification.
82
+// This is done by calling the given function with each notifiee. It is
83
+// meant to be called with your own type-safe notification functions:
84
+//
85
+// func (r *Rocket) Launch() {
86
+// r.notifyAll(func(n Notifiee) {
87
+// n.Launched(r)
88
+// })
89
+// }
90
+//
91
+// // make it private so only you can use it. This function is necessary
92
+// // to make sure you only up-cast in one place. You control who you added
93
+// // to be a notifiee. If Go adds generics, maybe we can get rid of this
94
+// // method but for now it is like wrapping a type-less container with
95
+// // a type safe interface.
96
+// func (r *Rocket) notifyAll(notify func(Notifiee)) {
97
+// r.notifier.NotifyAll(func(n notifier.Notifiee) {
98
+// notify(n.(Notifiee))
99
+// })
100
+// }
101
+//
102
+func (n *Notifier) NotifyAll(notify func(Notifiee)) {
103
+ n.mu.Lock()
104
+ if n.nots != nil { // so that zero-value is ready to be used.
105
+ for notifiee := range n.nots {
106
+ // we spin out a goroutine so that whatever the notification does
107
+ // it _never_ blocks out the client. This is so that consumers
108
+ // _cannot_ add hooks into your object that block you accidentally.
109
+ go notify(notifiee)
110
+ }
111
+ }
112
+ n.mu.Unlock()
113
+}
thirdparty/notifier/notifier_test.go
new
+207
@@ -0,0 +1,207 @@
1
+package notifier
2
+
3
+import (
4
+ "fmt"
5
+ "sync"
6
+ "testing"
7
+)
8
+
9
+// test data structures
10
+type Router struct {
11
+ queue chan Packet
12
+ notifier Notifier
13
+}
14
+
15
+type Packet struct{}
16
+
17
+type RouterNotifiee interface {
18
+ Enqueued(*Router, Packet)
19
+ Forwarded(*Router, Packet)
20
+ Dropped(*Router, Packet)
21
+}
22
+
23
+func (r *Router) Notify(n RouterNotifiee) {
24
+ r.notifier.Notify(n)
25
+}
26
+
27
+func (r *Router) StopNotify(n RouterNotifiee) {
28
+ r.notifier.StopNotify(n)
29
+}
30
+
31
+func (r *Router) notifyAll(notify func(n RouterNotifiee)) {
32
+ r.notifier.NotifyAll(func(n Notifiee) {
33
+ notify(n.(RouterNotifiee))
34
+ })
35
+}
36
+
37
+func (r *Router) Receive(p Packet) {
38
+
39
+ select {
40
+ case r.queue <- p: // enqueued
41
+ r.notifyAll(func(n RouterNotifiee) {
42
+ n.Enqueued(r, p)
43
+ })
44
+
45
+ default: // drop
46
+ r.notifyAll(func(n RouterNotifiee) {
47
+ n.Dropped(r, p)
48
+ })
49
+ }
50
+}
51
+
52
+func (r *Router) Forward() {
53
+ p := <-r.queue
54
+ r.notifyAll(func(n RouterNotifiee) {
55
+ n.Forwarded(r, p)
56
+ })
57
+}
58
+
59
+type Metrics struct {
60
+ enqueued int
61
+ forwarded int
62
+ dropped int
63
+ received chan struct{}
64
+ sync.Mutex
65
+}
66
+
67
+func (m *Metrics) Enqueued(*Router, Packet) {
68
+ m.Lock()
69
+ m.enqueued++
70
+ m.Unlock()
71
+ if m.received != nil {
72
+ m.received <- struct{}{}
73
+ }
74
+}
75
+
76
+func (m *Metrics) Forwarded(*Router, Packet) {
77
+ m.Lock()
78
+ m.forwarded++
79
+ m.Unlock()
80
+ if m.received != nil {
81
+ m.received <- struct{}{}
82
+ }
83
+}
84
+
85
+func (m *Metrics) Dropped(*Router, Packet) {
86
+ m.Lock()
87
+ m.dropped++
88
+ m.Unlock()
89
+ if m.received != nil {
90
+ m.received <- struct{}{}
91
+ }
92
+}
93
+
94
+func (m *Metrics) String() string {
95
+ m.Lock()
96
+ defer m.Unlock()
97
+ return fmt.Sprintf("%d enqueued, %d forwarded, %d in queue, %d dropped",
98
+ m.enqueued, m.forwarded, m.enqueued-m.forwarded, m.dropped)
99
+}
100
+
101
+func TestNotifies(t *testing.T) {
102
+
103
+ m := Metrics{received: make(chan struct{})}
104
+ r := Router{queue: make(chan Packet, 10)}
105
+ r.Notify(&m)
106
+
107
+ for i := 0; i < 10; i++ {
108
+ r.Receive(Packet{})
109
+ <-m.received
110
+ if m.enqueued != (1 + i) {
111
+ t.Error("not notifying correctly", m.enqueued, 1+i)
112
+ }
113
+
114
+ }
115
+
116
+ for i := 0; i < 10; i++ {
117
+ r.Receive(Packet{})
118
+ <-m.received
119
+ if m.enqueued != 10 {
120
+ t.Error("not notifying correctly", m.enqueued, 10)
121
+ }
122
+ if m.dropped != (1 + i) {
123
+ t.Error("not notifying correctly", m.dropped, 1+i)
124
+ }
125
+ }
126
+}
127
+
128
+func TestStopsNotifying(t *testing.T) {
129
+ m := Metrics{received: make(chan struct{})}
130
+ r := Router{queue: make(chan Packet, 10)}
131
+ r.Notify(&m)
132
+
133
+ for i := 0; i < 5; i++ {
134
+ r.Receive(Packet{})
135
+ <-m.received
136
+ if m.enqueued != (1 + i) {
137
+ t.Error("not notifying correctly")
138
+ }
139
+ }
140
+
141
+ r.StopNotify(&m)
142
+
143
+ for i := 0; i < 5; i++ {
144
+ r.Receive(Packet{})
145
+ select {
146
+ case <-m.received:
147
+ t.Error("did not stop notifying")
148
+ default:
149
+ }
150
+ if m.enqueued != 5 {
151
+ t.Error("did not stop notifying")
152
+ }
153
+ }
154
+}
155
+
156
+func TestThreadsafe(t *testing.T) {
157
+ N := 1000
158
+ r := Router{queue: make(chan Packet, 10)}
159
+ m1 := Metrics{received: make(chan struct{})}
160
+ m2 := Metrics{received: make(chan struct{})}
161
+ m3 := Metrics{received: make(chan struct{})}
162
+ r.Notify(&m1)
163
+ r.Notify(&m2)
164
+ r.Notify(&m3)
165
+
166
+ var n int
167
+ var wg sync.WaitGroup
168
+ for i := 0; i < N; i++ {
169
+ n++
170
+ wg.Add(1)
171
+ go func() {
172
+ defer wg.Done()
173
+ r.Receive(Packet{})
174
+ }()
175
+
176
+ if i%3 == 0 {
177
+ n++
178
+ wg.Add(1)
179
+ go func() {
180
+ defer wg.Done()
181
+ r.Forward()
182
+ }()
183
+ }
184
+ }
185
+
186
+ // drain queues
187
+ for i := 0; i < (n * 3); i++ {
188
+ select {
189
+ case <-m1.received:
190
+ case <-m2.received:
191
+ case <-m3.received:
192
+ }
193
+ }
194
+
195
+ wg.Wait()
196
+
197
+ // counts should be correct and all agree. and this should
198
+ // run fine under `go test -race -cpu=5`
199
+
200
+ t.Log("m1", m1.String())
201
+ t.Log("m2", m2.String())
202
+ t.Log("m3", m3.String())
203
+
204
+ if m1.String() != m2.String() || m2.String() != m3.String() {
205
+ t.Error("counts disagree")
206
+ }
207
+}