table.go 12 KB
Newer Older
1 2
// package kbucket implements a kademlia 'k-bucket' routing table.
package kbucket
3 4

import (
5
	"context"
6
	"errors"
7
	"fmt"
8
	"sync"
9
	"time"
10

11 12 13
	"github.com/libp2p/go-libp2p-core/peer"
	"github.com/libp2p/go-libp2p-core/peerstore"

George Antoniadis's avatar
George Antoniadis committed
14
	logging "github.com/ipfs/go-log"
15 16
)

Jeromy's avatar
Jeromy committed
17
var log = logging.Logger("table")
18

19 20 21
var ErrPeerRejectedHighLatency = errors.New("peer rejected; latency too high")
var ErrPeerRejectedNoCapacity = errors.New("peer rejected; insufficient capacity")

22 23
// RoutingTable defines the routing table.
type RoutingTable struct {
24
	// the routing table context
25
	ctx context.Context
26 27 28
	// function to cancel the RT context
	ctxCancel context.CancelFunc

29 30 31 32 33 34
	// ID of the local peer
	local ID

	// Blanket lock, refine later for better performance
	tabLock sync.RWMutex

35
	// latency metrics
36
	metrics peerstore.Metrics
37

38 39 40
	// Maximum acceptable latency for peers in this cluster
	maxLatency time.Duration

41
	// kBuckets define all the fingers to other nodes.
Aarsh Shah's avatar
Aarsh Shah committed
42
	buckets    []*bucket
43
	bucketsize int
Jeromy's avatar
Jeromy committed
44

Aarsh Shah's avatar
Aarsh Shah committed
45 46 47
	cplRefreshLk   sync.RWMutex
	cplRefreshedAt map[uint]time.Time

Jeromy's avatar
Jeromy committed
48 49 50
	// notification functions
	PeerRemoved func(peer.ID)
	PeerAdded   func(peer.ID)
51

Aarsh Shah's avatar
Aarsh Shah committed
52 53
	// usefulnessGracePeriod is the maximum grace period we will give to a
	// peer in the bucket to be useful to us, failing which, we will evict it to make place for a new peer if the bucket
54
	// is full
Aarsh Shah's avatar
Aarsh Shah committed
55
	usefulnessGracePeriod float64
56 57
}

Chas Leichner's avatar
Chas Leichner committed
58
// NewRoutingTable creates a new routing table with a given bucketsize, local ID, and latency tolerance.
Aarsh Shah's avatar
Aarsh Shah committed
59
func NewRoutingTable(bucketsize int, localID ID, latency time.Duration, m peerstore.Metrics, usefulnessGracePeriod float64) (*RoutingTable, error) {
Jeromy's avatar
Jeromy committed
60
	rt := &RoutingTable{
Aarsh Shah's avatar
Aarsh Shah committed
61
		buckets:    []*bucket{newBucket()},
62 63 64 65 66 67
		bucketsize: bucketsize,
		local:      localID,

		maxLatency: latency,
		metrics:    m,

Aarsh Shah's avatar
Aarsh Shah committed
68
		cplRefreshedAt: make(map[uint]time.Time),
69 70 71 72

		PeerRemoved: func(peer.ID) {},
		PeerAdded:   func(peer.ID) {},

Aarsh Shah's avatar
Aarsh Shah committed
73
		usefulnessGracePeriod: usefulnessGracePeriod,
Jeromy's avatar
Jeromy committed
74 75
	}

76
	rt.ctx, rt.ctxCancel = context.WithCancel(context.Background())
77 78 79 80

	return rt, nil
}

Aarsh Shah's avatar
Aarsh Shah committed
81 82 83
// Close shuts down the Routing Table & all associated processes.
// It is safe to call this multiple times.
func (rt *RoutingTable) Close() error {
84 85
	rt.ctxCancel()
	return nil
Aarsh Shah's avatar
Aarsh Shah committed
86 87
}

88
// TryAddPeer tries to add a peer to the Routing table. If the peer ALREADY exists in the Routing Table, this call is a no-op.
Aarsh Shah's avatar
Aarsh Shah committed
89
// If the peer is a queryPeer i.e. we queried it or it queried us, we set the LastSuccessfulOutboundQuery to the current time.
90
// If the peer is just a peer that we connect to/it connected to us without any DHT query, we consider it as having
Aarsh Shah's avatar
Aarsh Shah committed
91
// no LastSuccessfulOutboundQuery.
92 93
//
// If the logical bucket to which the peer belongs is full and it's not the last bucket, we try to replace an existing peer
Aarsh Shah's avatar
Aarsh Shah committed
94
// whose LastSuccessfulOutboundQuery is above the maximum allowed threshold in that bucket with the new peer.
95 96 97 98 99 100 101 102
// If no such peer exists in that bucket, we do NOT add the peer to the Routing Table and return error "ErrPeerRejectedNoCapacity".

// It returns a boolean value set to true if the peer was newly added to the Routing Table, false otherwise.
// It also returns any error that occurred while adding the peer to the Routing Table. If the error is not nil,
// the boolean value will ALWAYS be false i.e. the peer wont be added to the Routing Table it it's not already there.
//
// A return value of false with error=nil indicates that the peer ALREADY exists in the Routing Table.
func (rt *RoutingTable) TryAddPeer(p peer.ID, queryPeer bool) (bool, error) {
103 104
	rt.tabLock.Lock()
	defer rt.tabLock.Unlock()
105

106
	return rt.addPeer(p, queryPeer)
107 108 109
}

// locking is the responsibility of the caller
110
func (rt *RoutingTable) addPeer(p peer.ID, queryPeer bool) (bool, error) {
111
	bucketID := rt.bucketIdForPeer(p)
Aarsh Shah's avatar
Aarsh Shah committed
112
	bucket := rt.buckets[bucketID]
Aarsh Shah's avatar
Aarsh Shah committed
113
	var lastUsefulAt time.Time
114
	if queryPeer {
Aarsh Shah's avatar
Aarsh Shah committed
115
		lastUsefulAt = time.Now()
116
	}
117

118 119 120
	// peer already exists in the Routing Table.
	if peer := bucket.getPeer(p); peer != nil {
		return false, nil
121 122
	}

123
	// peer's latency threshold is NOT acceptable
124 125
	if rt.metrics.LatencyEWMA(p) > rt.maxLatency {
		// Connection doesnt meet requirements, skip!
126
		return false, ErrPeerRejectedHighLatency
127 128 129
	}

	// We have enough space in the bucket (whether spawned or grouped).
130
	if bucket.len() < rt.bucketsize {
Aarsh Shah's avatar
Aarsh Shah committed
131 132
		bucket.pushFront(&PeerInfo{Id: p, LastUsefulAt: lastUsefulAt, LastSuccessfulOutboundQueryAt: time.Now(),
			dhtId: ConvertPeerID(p)})
133
		rt.PeerAdded(p)
134
		return true, nil
135 136
	}

Aarsh Shah's avatar
Aarsh Shah committed
137
	if bucketID == len(rt.buckets)-1 {
138 139 140
		// if the bucket is too large and this is the last bucket (i.e. wildcard), unfold it.
		rt.nextBucket()
		// the structure of the table has changed, so let's recheck if the peer now has a dedicated bucket.
141
		bucketID = rt.bucketIdForPeer(p)
Aarsh Shah's avatar
Aarsh Shah committed
142
		bucket = rt.buckets[bucketID]
143 144 145

		// push the peer only if the bucket isn't overflowing after slitting
		if bucket.len() < rt.bucketsize {
Aarsh Shah's avatar
Aarsh Shah committed
146 147
			bucket.pushFront(&PeerInfo{Id: p, LastUsefulAt: lastUsefulAt, LastSuccessfulOutboundQueryAt: time.Now(),
				dhtId: ConvertPeerID(p)})
148
			rt.PeerAdded(p)
149 150 151 152 153
			return true, nil
		}
	}

	// the bucket to which the peer belongs is full. Let's try to find a peer
Aarsh Shah's avatar
Aarsh Shah committed
154
	// in that bucket with a LastSuccessfulOutboundQuery value above the maximum threshold and replace it.
155 156
	allPeers := bucket.peers()
	for _, pc := range allPeers {
Aarsh Shah's avatar
Aarsh Shah committed
157
		if float64(time.Since(pc.LastUsefulAt)) > rt.usefulnessGracePeriod {
158 159
			// let's evict it and add the new peer
			if bucket.remove(pc.Id) {
Aarsh Shah's avatar
Aarsh Shah committed
160 161
				bucket.pushFront(&PeerInfo{Id: p, LastUsefulAt: lastUsefulAt, LastSuccessfulOutboundQueryAt: time.Now(),
					dhtId: ConvertPeerID(p)})
162 163 164
				rt.PeerAdded(p)
				return true, nil
			}
165 166
		}
	}
167

168 169 170
	return false, ErrPeerRejectedNoCapacity
}

Aarsh Shah's avatar
Aarsh Shah committed
171 172 173 174 175 176 177 178 179 180 181 182 183 184
// GetPeerInfos returns the peer information that we've stored in the buckets
func (rt *RoutingTable) GetPeerInfos() []PeerInfo {
	rt.tabLock.RLock()
	defer rt.tabLock.RUnlock()

	var pis []PeerInfo
	for _, b := range rt.buckets {
		for _, p := range b.peers() {
			pis = append(pis, p)
		}
	}
	return pis
}

Aarsh Shah's avatar
Aarsh Shah committed
185
// UpdateLastSuccessfulOutboundQuery updates the LastSuccessfulOutboundQueryAt time of the peer.
186
// Returns true if the update was successful, false otherwise.
Aarsh Shah's avatar
Aarsh Shah committed
187
func (rt *RoutingTable) UpdateLastSuccessfulOutboundQueryAt(p peer.ID, t time.Time) bool {
188 189 190 191 192 193 194
	rt.tabLock.Lock()
	defer rt.tabLock.Unlock()

	bucketID := rt.bucketIdForPeer(p)
	bucket := rt.buckets[bucketID]

	if pc := bucket.getPeer(p); pc != nil {
Aarsh Shah's avatar
Aarsh Shah committed
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
		pc.LastSuccessfulOutboundQueryAt = t
		return true
	}
	return false
}

// UpdateLastUsefulAt updates the LastUsefulAt time of the peer.
// Returns true if the update was successful, false otherwise.
func (rt *RoutingTable) UpdateLastUsefulAt(p peer.ID, t time.Time) bool {
	rt.tabLock.Lock()
	defer rt.tabLock.Unlock()

	bucketID := rt.bucketIdForPeer(p)
	bucket := rt.buckets[bucketID]

	if pc := bucket.getPeer(p); pc != nil {
		pc.LastUsefulAt = t
212 213 214
		return true
	}
	return false
215 216
}

217 218 219 220
// RemovePeer should be called when the caller is sure that a peer is not useful for queries.
// For eg: the peer could have stopped supporting the DHT protocol.
// It evicts the peer from the Routing Table.
func (rt *RoutingTable) RemovePeer(p peer.ID) {
Steven Allen's avatar
Steven Allen committed
221 222
	rt.tabLock.Lock()
	defer rt.tabLock.Unlock()
223 224 225 226 227
	rt.removePeer(p)
}

// locking is the responsibility of the caller
func (rt *RoutingTable) removePeer(p peer.ID) {
228
	bucketID := rt.bucketIdForPeer(p)
Aarsh Shah's avatar
Aarsh Shah committed
229
	bucket := rt.buckets[bucketID]
230
	if bucket.remove(p) {
231
		// peer removed callback
232
		rt.PeerRemoved(p)
233
		return
234
	}
235 236
}

237
func (rt *RoutingTable) nextBucket() {
238 239 240
	// This is the last bucket, which allegedly is a mixed bag containing peers not belonging in dedicated (unfolded) buckets.
	// _allegedly_ is used here to denote that *all* peers in the last bucket might feasibly belong to another bucket.
	// This could happen if e.g. we've unfolded 4 buckets, and all peers in folded bucket 5 really belong in bucket 8.
Aarsh Shah's avatar
Aarsh Shah committed
241 242 243
	bucket := rt.buckets[len(rt.buckets)-1]
	newBucket := bucket.split(len(rt.buckets)-1, rt.local)
	rt.buckets = append(rt.buckets, newBucket)
244

245
	// The newly formed bucket still contains too many peers. We probably just unfolded a empty bucket.
246
	if newBucket.len() >= rt.bucketsize {
247 248
		// Keep unfolding the table until the last bucket is not overflowing.
		rt.nextBucket()
249 250 251
	}
}

Jeromy's avatar
Jeromy committed
252
// Find a specific peer by ID or return nil
253
func (rt *RoutingTable) Find(id peer.ID) peer.ID {
Chas Leichner's avatar
Chas Leichner committed
254
	srch := rt.NearestPeers(ConvertPeerID(id), 1)
255 256
	if len(srch) == 0 || srch[0] != id {
		return ""
Jeromy's avatar
Jeromy committed
257 258 259 260
	}
	return srch[0]
}

261
// NearestPeer returns a single peer that is nearest to the given ID
262
func (rt *RoutingTable) NearestPeer(id ID) peer.ID {
263 264 265 266
	peers := rt.NearestPeers(id, 1)
	if len(peers) > 0 {
		return peers[0]
	}
267

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
268
	log.Debugf("NearestPeer: Returning nil, table size = %d", rt.Size())
269
	return ""
270 271
}

272
// NearestPeers returns a list of the 'count' closest peers to the given ID
273
func (rt *RoutingTable) NearestPeers(id ID, count int) []peer.ID {
274 275 276 277
	// This is the number of bits _we_ share with the key. All peers in this
	// bucket share cpl bits with us and will therefore share at least cpl+1
	// bits with the given key. +1 because both the target and all peers in
	// this bucket differ from us in the cpl bit.
Matt Joiner's avatar
Matt Joiner committed
278
	cpl := CommonPrefixLen(id, rt.local)
279

280
	// It's assumed that this also protects the buckets.
Jeromy's avatar
Jeromy committed
281 282
	rt.tabLock.RLock()

Aarsh Shah's avatar
Aarsh Shah committed
283 284 285
	// Get bucket index or last bucket
	if cpl >= len(rt.buckets) {
		cpl = len(rt.buckets) - 1
286 287
	}

288
	pds := peerDistanceSorter{
289
		peers:  make([]peerDistance, 0, count+rt.bucketsize),
290 291
		target: id,
	}
292

293
	// Add peers from the target bucket (cpl+1 shared bits).
Aarsh Shah's avatar
Aarsh Shah committed
294
	pds.appendPeersFromList(rt.buckets[cpl].list)
295

Steven Allen's avatar
Steven Allen committed
296 297 298
	// If we're short, add peers from all buckets to the right. All buckets
	// to the right share exactly cpl bits (as opposed to the cpl+1 bits
	// shared by the peers in the cpl bucket).
299
	//
Steven Allen's avatar
Steven Allen committed
300 301 302
	// This is, unfortunately, less efficient than we'd like. We will switch
	// to a trie implementation eventually which will allow us to find the
	// closest N peers to any target key.
303

Steven Allen's avatar
Steven Allen committed
304 305 306 307
	if pds.Len() < count {
		for i := cpl + 1; i < len(rt.buckets); i++ {
			pds.appendPeersFromList(rt.buckets[i].list)
		}
308 309
	}

310 311 312 313
	// If we're still short, add in buckets that share _fewer_ bits. We can
	// do this bucket by bucket because each bucket will share 1 fewer bit
	// than the last.
	//
Aarsh Shah's avatar
fix doc  
Aarsh Shah committed
314 315
	// * bucket cpl-1: cpl-1 shared bits.
	// * bucket cpl-2: cpl-2 shared bits.
316
	// ...
317
	for i := cpl - 1; i >= 0 && pds.Len() < count; i-- {
Aarsh Shah's avatar
Aarsh Shah committed
318
		pds.appendPeersFromList(rt.buckets[i].list)
319
	}
Jeromy's avatar
Jeromy committed
320
	rt.tabLock.RUnlock()
321 322

	// Sort by distance to local peer
323
	pds.sort()
324

325 326
	if count < pds.Len() {
		pds.peers = pds.peers[:count]
327 328
	}

329 330
	out := make([]peer.ID, 0, pds.Len())
	for _, p := range pds.peers {
331
		out = append(out, p.p)
332 333 334 335 336
	}

	return out
}

337
// Size returns the total number of peers in the routing table
338 339
func (rt *RoutingTable) Size() int {
	var tot int
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
340
	rt.tabLock.RLock()
Aarsh Shah's avatar
Aarsh Shah committed
341
	for _, buck := range rt.buckets {
342
		tot += buck.len()
343
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
344
	rt.tabLock.RUnlock()
345 346 347
	return tot
}

Chas Leichner's avatar
Chas Leichner committed
348
// ListPeers takes a RoutingTable and returns a list of all peers from all buckets in the table.
349
func (rt *RoutingTable) ListPeers() []peer.ID {
350
	rt.tabLock.RLock()
351 352 353
	defer rt.tabLock.RUnlock()

	var peers []peer.ID
Aarsh Shah's avatar
Aarsh Shah committed
354
	for _, buck := range rt.buckets {
355
		peers = append(peers, buck.peerIds()...)
356 357 358
	}
	return peers
}
359

Chas Leichner's avatar
Chas Leichner committed
360 361
// Print prints a descriptive statement about the provided RoutingTable
func (rt *RoutingTable) Print() {
362 363
	fmt.Printf("Routing Table, bs = %d, Max latency = %d\n", rt.bucketsize, rt.maxLatency)
	rt.tabLock.RLock()
364

Aarsh Shah's avatar
Aarsh Shah committed
365
	for i, b := range rt.buckets {
366 367 368
		fmt.Printf("\tbucket: %d\n", i)

		for e := b.list.Front(); e != nil; e = e.Next() {
Aarsh Shah's avatar
Aarsh Shah committed
369
			p := e.Value.(*PeerInfo).Id
370 371
			fmt.Printf("\t\t- %s %s\n", p.Pretty(), rt.metrics.LatencyEWMA(p).String())
		}
372
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
373
	rt.tabLock.RUnlock()
374
}
375 376 377 378 379 380

// the caller is responsible for the locking
func (rt *RoutingTable) bucketIdForPeer(p peer.ID) int {
	peerID := ConvertPeerID(p)
	cpl := CommonPrefixLen(peerID, rt.local)
	bucketID := cpl
Aarsh Shah's avatar
Aarsh Shah committed
381 382
	if bucketID >= len(rt.buckets) {
		bucketID = len(rt.buckets) - 1
383 384 385
	}
	return bucketID
}
386 387 388 389 390 391 392 393 394 395 396 397 398 399

// maxCommonPrefix returns the maximum common prefix length between any peer in
// the table and the current peer.
func (rt *RoutingTable) maxCommonPrefix() uint {
	rt.tabLock.RLock()
	defer rt.tabLock.RUnlock()

	for i := len(rt.buckets) - 1; i >= 0; i-- {
		if rt.buckets[i].len() > 0 {
			return rt.buckets[i].maxCommonPrefix(rt.local)
		}
	}
	return 0
}