-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
backoffcache.go
329 lines (277 loc) · 7.37 KB
/
backoffcache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package backoff
import (
"context"
"fmt"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/discovery"
"github.com/libp2p/go-libp2p/core/peer"
ma "github.com/multiformats/go-multiaddr"
)
// BackoffDiscovery is an implementation of discovery that caches peer data and attenuates repeated queries
type BackoffDiscovery struct {
disc discovery.Discovery
stratFactory BackoffFactory
peerCache map[string]*backoffCache
peerCacheMux sync.RWMutex
parallelBufSz int
returnedBufSz int
clock clock
}
type BackoffDiscoveryOption func(*BackoffDiscovery) error
func NewBackoffDiscovery(disc discovery.Discovery, stratFactory BackoffFactory, opts ...BackoffDiscoveryOption) (discovery.Discovery, error) {
b := &BackoffDiscovery{
disc: disc,
stratFactory: stratFactory,
peerCache: make(map[string]*backoffCache),
parallelBufSz: 32,
returnedBufSz: 32,
clock: realClock{},
}
for _, opt := range opts {
if err := opt(b); err != nil {
return nil, err
}
}
return b, nil
}
// WithBackoffDiscoverySimultaneousQueryBufferSize sets the buffer size for the channels between the main FindPeers query
// for a given namespace and all simultaneous FindPeers queries for the namespace
func WithBackoffDiscoverySimultaneousQueryBufferSize(size int) BackoffDiscoveryOption {
return func(b *BackoffDiscovery) error {
if size < 0 {
return fmt.Errorf("cannot set size to be smaller than 0")
}
b.parallelBufSz = size
return nil
}
}
// WithBackoffDiscoveryReturnedChannelSize sets the size of the buffer to be used during a FindPeer query.
// Note: This does not apply if the query occurs during the backoff time
func WithBackoffDiscoveryReturnedChannelSize(size int) BackoffDiscoveryOption {
return func(b *BackoffDiscovery) error {
if size < 0 {
return fmt.Errorf("cannot set size to be smaller than 0")
}
b.returnedBufSz = size
return nil
}
}
type clock interface {
Now() time.Time
}
type realClock struct{}
func (c realClock) Now() time.Time {
return time.Now()
}
type backoffCache struct {
// strat is assigned on creation and not written to
strat BackoffStrategy
mux sync.Mutex // guards writes to all following fields
nextDiscover time.Time
prevPeers map[peer.ID]peer.AddrInfo
peers map[peer.ID]peer.AddrInfo
sendingChs map[chan peer.AddrInfo]int
ongoing bool
clock clock
}
func (d *BackoffDiscovery) Advertise(ctx context.Context, ns string, opts ...discovery.Option) (time.Duration, error) {
return d.disc.Advertise(ctx, ns, opts...)
}
func (d *BackoffDiscovery) FindPeers(ctx context.Context, ns string, opts ...discovery.Option) (<-chan peer.AddrInfo, error) {
// Get options
var options discovery.Options
err := options.Apply(opts...)
if err != nil {
return nil, err
}
// Get cached peers
d.peerCacheMux.RLock()
c, ok := d.peerCache[ns]
d.peerCacheMux.RUnlock()
/*
Overall plan:
If it's time to look for peers, look for peers, then return them
If it's not time then return cache
If it's time to look for peers, but we have already started looking. Get up to speed with ongoing request
*/
// Setup cache if we don't have one yet
if !ok {
pc := &backoffCache{
nextDiscover: time.Time{},
prevPeers: make(map[peer.ID]peer.AddrInfo),
peers: make(map[peer.ID]peer.AddrInfo),
sendingChs: make(map[chan peer.AddrInfo]int),
strat: d.stratFactory(),
clock: d.clock,
}
d.peerCacheMux.Lock()
c, ok = d.peerCache[ns]
if !ok {
d.peerCache[ns] = pc
c = pc
}
d.peerCacheMux.Unlock()
}
c.mux.Lock()
defer c.mux.Unlock()
timeExpired := d.clock.Now().After(c.nextDiscover)
// If it's not yet time to search again and no searches are in progress then return cached peers
if !(timeExpired || c.ongoing) {
chLen := options.Limit
if chLen == 0 {
chLen = len(c.prevPeers)
} else if chLen > len(c.prevPeers) {
chLen = len(c.prevPeers)
}
pch := make(chan peer.AddrInfo, chLen)
for _, ai := range c.prevPeers {
select {
case pch <- ai:
default:
// skip if we have asked for a lower limit than the number of peers known
}
}
close(pch)
return pch, nil
}
// If a request is not already in progress setup a dispatcher channel for dispatching incoming peers
if !c.ongoing {
pch, err := d.disc.FindPeers(ctx, ns, opts...)
if err != nil {
return nil, err
}
c.ongoing = true
go findPeerDispatcher(ctx, c, pch)
}
// Setup receiver channel for receiving peers from ongoing requests
evtCh := make(chan peer.AddrInfo, d.parallelBufSz)
pch := make(chan peer.AddrInfo, d.returnedBufSz)
rcvPeers := make([]peer.AddrInfo, 0, 32)
for _, ai := range c.peers {
rcvPeers = append(rcvPeers, ai)
}
c.sendingChs[evtCh] = options.Limit
go findPeerReceiver(ctx, pch, evtCh, rcvPeers)
return pch, nil
}
func findPeerDispatcher(ctx context.Context, c *backoffCache, pch <-chan peer.AddrInfo) {
defer func() {
c.mux.Lock()
// If the peer addresses have changed reset the backoff
if checkUpdates(c.prevPeers, c.peers) {
c.strat.Reset()
c.prevPeers = c.peers
}
c.nextDiscover = c.clock.Now().Add(c.strat.Delay())
c.ongoing = false
c.peers = make(map[peer.ID]peer.AddrInfo)
for ch := range c.sendingChs {
close(ch)
}
c.sendingChs = make(map[chan peer.AddrInfo]int)
c.mux.Unlock()
}()
for {
select {
case ai, ok := <-pch:
if !ok {
return
}
c.mux.Lock()
// If we receive the same peer multiple times return the address union
var sendAi peer.AddrInfo
if prevAi, ok := c.peers[ai.ID]; ok {
if combinedAi := mergeAddrInfos(prevAi, ai); combinedAi != nil {
sendAi = *combinedAi
} else {
c.mux.Unlock()
continue
}
} else {
sendAi = ai
}
c.peers[ai.ID] = sendAi
for ch, rem := range c.sendingChs {
if rem > 0 {
ch <- sendAi
c.sendingChs[ch] = rem - 1
}
}
c.mux.Unlock()
case <-ctx.Done():
return
}
}
}
func findPeerReceiver(ctx context.Context, pch, evtCh chan peer.AddrInfo, rcvPeers []peer.AddrInfo) {
defer close(pch)
for {
select {
case ai, ok := <-evtCh:
if ok {
rcvPeers = append(rcvPeers, ai)
sentAll := true
sendPeers:
for i, p := range rcvPeers {
select {
case pch <- p:
default:
rcvPeers = rcvPeers[i:]
sentAll = false
break sendPeers
}
}
if sentAll {
rcvPeers = []peer.AddrInfo{}
}
} else {
for _, p := range rcvPeers {
select {
case pch <- p:
case <-ctx.Done():
return
}
}
return
}
case <-ctx.Done():
return
}
}
}
func mergeAddrInfos(prevAi, newAi peer.AddrInfo) *peer.AddrInfo {
seen := make(map[string]struct{}, len(prevAi.Addrs))
combinedAddrs := make([]ma.Multiaddr, 0, len(prevAi.Addrs))
addAddrs := func(addrs []ma.Multiaddr) {
for _, addr := range addrs {
if _, ok := seen[addr.String()]; ok {
continue
}
seen[addr.String()] = struct{}{}
combinedAddrs = append(combinedAddrs, addr)
}
}
addAddrs(prevAi.Addrs)
addAddrs(newAi.Addrs)
if len(combinedAddrs) > len(prevAi.Addrs) {
combinedAi := &peer.AddrInfo{ID: prevAi.ID, Addrs: combinedAddrs}
return combinedAi
}
return nil
}
func checkUpdates(orig, update map[peer.ID]peer.AddrInfo) bool {
if len(orig) != len(update) {
return true
}
for p, ai := range update {
if prevAi, ok := orig[p]; ok {
if combinedAi := mergeAddrInfos(prevAi, ai); combinedAi != nil {
return true
}
} else {
return true
}
}
return false
}