Skip to content

Commit 6530669

Browse files
committed
gossip component- datastructures
This commit adds 2 datastructures used by the gossip forwarding mechanism: - messageStore: A storage cache for disseminated data items It is used to decide whether a message is "fresh", and needs to be forwarded to other peers, or maybe if its too "old" and it needs to be discarded. This cache is also in use by the pull mechanism the gossip code that'll be pushed later on will connect between them because it provides the data source for answering pull requests from other peers. - forwardingBatcher: A batch and time based component that causes the gossip to forward messages When a timer expires or if too many messages are outstanding. Change-Id: Ic23e9c168f6f387e85a0a10b8b0edf5ba30617ee Signed-off-by: Yacov Manevich <[email protected]>
1 parent 8a40a51 commit 6530669

File tree

4 files changed

+511
-0
lines changed

4 files changed

+511
-0
lines changed

gossip/gossip/batcher.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
Copyright IBM Corp. 2016 All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gossip
18+
19+
import (
20+
"sync"
21+
"sync/atomic"
22+
"time"
23+
)
24+
25+
type emitBatchCallback func([]interface{})
26+
27+
//batchingEmitter is used for the gossip push/forwarding phase.
28+
// Messages are added into the batchingEmitter, and they are forwarded periodically T times in batches and then discarded.
29+
// If the batchingEmitter's stored message count reaches a certain capacity, that also triggers a message dispatch
30+
type batchingEmitter interface {
31+
// Add adds a message to be batched
32+
Add(interface{})
33+
34+
// Stop stops the component
35+
Stop()
36+
37+
// Size returns the amount of pending messages to be emitted
38+
Size() int
39+
}
40+
41+
// newBatchingEmitter accepts the following parameters:
42+
// iterations: number of times each message is forwarded
43+
// burstSize: a threshold that triggers a forwarding because of message count
44+
// latency: the maximum delay that each message can be stored without being forwarded
45+
// cb: a callback that is called in order for the forwarding to take place
46+
func newBatchingEmitter(iterations, burstSize int, latency time.Duration, cb emitBatchCallback) batchingEmitter {
47+
p := &batchingEmitterImpl{
48+
cb: cb,
49+
delay: latency,
50+
iterations: iterations,
51+
burstSize: burstSize,
52+
lock: &sync.Mutex{},
53+
buff: make([]*batchedMessage, 0),
54+
stopFlag: int32(0),
55+
}
56+
57+
go p.periodicEmit()
58+
return p
59+
}
60+
61+
func (p *batchingEmitterImpl) periodicEmit() {
62+
for !p.toDie() {
63+
time.Sleep(p.delay)
64+
p.lock.Lock()
65+
p.emit()
66+
p.lock.Unlock()
67+
}
68+
}
69+
70+
func (p *batchingEmitterImpl) emit() {
71+
if len(p.buff) == 0 {
72+
return
73+
}
74+
msgs2beEmitted := make([]interface{}, len(p.buff))
75+
for i, v := range p.buff {
76+
msgs2beEmitted[i] = v.data
77+
}
78+
79+
p.cb(msgs2beEmitted)
80+
p.decrementCounters()
81+
}
82+
83+
func (p *batchingEmitterImpl) decrementCounters() {
84+
n := len(p.buff)
85+
for i := 0; i < n; i++ {
86+
msg := p.buff[i]
87+
msg.iterationsLeft--
88+
if msg.iterationsLeft == 0 {
89+
p.buff = append(p.buff[:i], p.buff[i+1:]...)
90+
n--
91+
i--
92+
}
93+
}
94+
}
95+
96+
func (p *batchingEmitterImpl) toDie() bool {
97+
return atomic.LoadInt32(&(p.stopFlag)) == int32(1)
98+
}
99+
100+
type batchingEmitterImpl struct {
101+
iterations int
102+
burstSize int
103+
delay time.Duration
104+
cb emitBatchCallback
105+
lock *sync.Mutex
106+
buff []*batchedMessage
107+
stopFlag int32
108+
}
109+
110+
type batchedMessage struct {
111+
data interface{}
112+
iterationsLeft int
113+
}
114+
115+
func (p *batchingEmitterImpl) Stop() {
116+
atomic.StoreInt32(&(p.stopFlag), int32(1))
117+
}
118+
119+
func (p *batchingEmitterImpl) Size() int {
120+
p.lock.Lock()
121+
defer p.lock.Unlock()
122+
return len(p.buff)
123+
}
124+
125+
func (p *batchingEmitterImpl) Add(message interface{}) {
126+
p.lock.Lock()
127+
defer p.lock.Unlock()
128+
129+
p.buff = append(p.buff, &batchedMessage{data: message, iterationsLeft: p.iterations})
130+
131+
if len(p.buff) >= p.burstSize {
132+
p.emit()
133+
}
134+
}

gossip/gossip/batcher_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
Copyright IBM Corp. 2016 All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gossip
18+
19+
import (
20+
"github.com/stretchr/testify/assert"
21+
"sync"
22+
"sync/atomic"
23+
"testing"
24+
"time"
25+
)
26+
27+
func TestBatchingEmitterAddAndSize(t *testing.T) {
28+
emitter := newBatchingEmitter(1, 10, time.Second, func(a []interface{}) {})
29+
defer emitter.Stop()
30+
emitter.Add(1)
31+
emitter.Add(2)
32+
emitter.Add(3)
33+
assert.Equal(t, 3, emitter.Size())
34+
}
35+
36+
func TestBatchingEmitterStop(t *testing.T) {
37+
// In this test we make sure the emitter doesn't do anything after it's stopped
38+
disseminationAttempts := int32(0)
39+
cb := func(a []interface{}) {
40+
atomic.AddInt32(&disseminationAttempts, int32(1))
41+
}
42+
43+
emitter := newBatchingEmitter(10, 1, time.Duration(10)*time.Millisecond, cb)
44+
emitter.Add(1)
45+
time.Sleep(time.Duration(10) * time.Millisecond)
46+
emitter.Stop()
47+
time.Sleep(time.Duration(100) * time.Millisecond)
48+
assert.True(t, atomic.LoadInt32(&disseminationAttempts) < int32(5))
49+
}
50+
51+
func TestBatchingEmitterExpiration(t *testing.T) {
52+
// In this test we make sure that a message is expired and is discarded after enough time
53+
// and that it was forwarded an adequate amount of times
54+
disseminationAttempts := int32(0)
55+
cb := func(a []interface{}) {
56+
atomic.AddInt32(&disseminationAttempts, int32(1))
57+
}
58+
59+
emitter := newBatchingEmitter(10, 1, time.Duration(1)*time.Millisecond, cb)
60+
defer emitter.Stop()
61+
62+
emitter.Add(1)
63+
time.Sleep(time.Duration(50) * time.Millisecond)
64+
assert.Equal(t, int32(10), atomic.LoadInt32(&disseminationAttempts), "Inadaquate amount of dissemination attempts detected")
65+
assert.Equal(t, 0, emitter.Size())
66+
}
67+
68+
func TestBatchingEmitterCounter(t *testing.T) {
69+
// In this test we count the number of times each message is forwarded, with relation to the time passed
70+
counters := make(map[int]int)
71+
lock := &sync.Mutex{}
72+
cb := func(a []interface{}) {
73+
lock.Lock()
74+
defer lock.Unlock()
75+
for _, e := range a {
76+
n := e.(int)
77+
if _, exists := counters[n]; !exists {
78+
counters[n] = 0
79+
} else {
80+
counters[n]++
81+
}
82+
}
83+
}
84+
85+
emitter := newBatchingEmitter(5, 100, time.Duration(50)*time.Millisecond, cb)
86+
defer emitter.Stop()
87+
88+
for i := 1; i <= 5; i++ {
89+
emitter.Add(i)
90+
if i == 5 {
91+
break
92+
}
93+
time.Sleep(time.Duration(60) * time.Millisecond)
94+
}
95+
emitter.Stop()
96+
97+
lock.Lock()
98+
assert.Equal(t, 0, counters[4])
99+
assert.Equal(t, 1, counters[3])
100+
assert.Equal(t, 2, counters[2])
101+
assert.Equal(t, 3, counters[1])
102+
lock.Unlock()
103+
}
104+
105+
// TestBatchingEmitterBurstSizeCap tests that the emitter
106+
func TestBatchingEmitterBurstSizeCap(t *testing.T) {
107+
disseminationAttempts := int32(0)
108+
cb := func(a []interface{}) {
109+
atomic.AddInt32(&disseminationAttempts, int32(1))
110+
}
111+
emitter := newBatchingEmitter(1, 10, time.Duration(800)*time.Millisecond, cb)
112+
defer emitter.Stop()
113+
114+
for i := 0; i < 50; i++ {
115+
emitter.Add(i)
116+
}
117+
assert.Equal(t, int32(5), atomic.LoadInt32(&disseminationAttempts))
118+
}

gossip/gossip/msgs.go

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
Copyright IBM Corp. 2016 All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gossip
18+
19+
import "sync"
20+
21+
type invalidationResult int
22+
23+
const (
24+
MESSAGE_NO_ACTION = invalidationResult(0)
25+
MESSAGE_INVALIDATES = invalidationResult(1)
26+
MESSAGE_INVALIDATED = invalidationResult(2)
27+
)
28+
29+
// Returns:
30+
// MESSAGE_INVALIDATES if this message invalidates that
31+
// MESSAGE_INVALIDATED if this message is invalidated by that
32+
// MESSAGE_NO_ACTION otherwise
33+
type messageReplacingPolicy func(this interface{}, that interface{}) invalidationResult
34+
35+
// invalidationTrigger is invoked on each message that was invalidated because of a message addition
36+
// i.e: if add(0), add(1) was called one after the other, and the store has only {1} after the sequence of invocations
37+
// then the invalidation trigger on 0 was called when 1 was added.
38+
type invalidationTrigger func(message interface{})
39+
40+
func newMessageStore(pol messageReplacingPolicy, trigger invalidationTrigger) messageStore {
41+
return &messageStoreImpl{pol: pol, lock: &sync.RWMutex{}, messages: make([]*msg, 0), invTrigger: trigger}
42+
}
43+
44+
// messageStore adds messages to an internal buffer.
45+
// When a message is received, it might:
46+
// - Be added to the buffer
47+
// - Discarded because of some message already in the buffer (invalidated)
48+
// - Make a message already in the buffer to be discarded (invalidates)
49+
// When a message is invalidated, the invalidationTrigger is invoked on that message.
50+
type messageStore interface {
51+
// add adds a message to the store
52+
// returns true or false whether the message was added to the store
53+
add(msg interface{}) bool
54+
55+
// size returns the amount of messages in the store
56+
size() int
57+
58+
// get returns all messages in the store
59+
get() []interface{}
60+
}
61+
62+
type messageStoreImpl struct {
63+
pol messageReplacingPolicy
64+
lock *sync.RWMutex
65+
messages []*msg
66+
invTrigger invalidationTrigger
67+
}
68+
69+
type msg struct {
70+
data interface{}
71+
}
72+
73+
// add adds a message to the store
74+
func (s *messageStoreImpl) add(message interface{}) bool {
75+
s.lock.Lock()
76+
defer s.lock.Unlock()
77+
78+
n := len(s.messages)
79+
for i := 0; i < n; i++ {
80+
m := s.messages[i]
81+
switch s.pol(message, m.data) {
82+
case MESSAGE_INVALIDATED:
83+
return false
84+
break
85+
case MESSAGE_INVALIDATES:
86+
s.invTrigger(m.data)
87+
s.messages = append(s.messages[:i], s.messages[i+1:]...)
88+
n--
89+
i--
90+
break
91+
default:
92+
break
93+
}
94+
}
95+
96+
s.messages = append(s.messages, &msg{data: message})
97+
return true
98+
}
99+
100+
// size returns the amount of messages in the store
101+
func (s *messageStoreImpl) size() int {
102+
s.lock.RLock()
103+
defer s.lock.RUnlock()
104+
return len(s.messages)
105+
}
106+
107+
// get returns all messages in the store
108+
func (s *messageStoreImpl) get() []interface{} {
109+
s.lock.RLock()
110+
defer s.lock.RUnlock()
111+
112+
n := len(s.messages)
113+
res := make([]interface{}, n)
114+
for i := 0; i < n; i++ {
115+
res[i] = s.messages[i].data
116+
}
117+
return res
118+
}

0 commit comments

Comments
 (0)