Skip to content

Commit 5ebdff1

Browse files
committed
fix(kad-dht): prefer connected peers to reduce unnecessary dials
1 parent 3565a2e commit 5ebdff1

4 files changed

Lines changed: 116 additions & 13 deletions

File tree

packages/kad-dht/src/content-routing/index.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,24 @@ export class ContentRouting {
151151
finalPeerEvents.push(event)
152152
}
153153

154-
finalPeerEvents.forEach(event => {
155-
queue.add(async () => {
156-
for await (const notifyEvent of publishProviderRecord(event)) {
157-
events.push(notifyEvent)
158-
}
154+
// sort connected peers first to reuse existing connections before
155+
// opening new ones when sending ADD_PROVIDER records
156+
finalPeerEvents
157+
.sort((a, b) => {
158+
const aConnected = (self.components.connectionManager.getConnections(a.peer.id) ?? []).length > 0 ? 0 : 1
159+
const bConnected = (self.components.connectionManager.getConnections(b.peer.id) ?? []).length > 0 ? 0 : 1
160+
return aConnected - bConnected
159161
})
160-
.catch(err => {
161-
this.log.error('error publishing provider record to peer - %e', err)
162+
.forEach(event => {
163+
queue.add(async () => {
164+
for await (const notifyEvent of publishProviderRecord(event)) {
165+
events.push(notifyEvent)
166+
}
162167
})
163-
})
168+
.catch(err => {
169+
this.log.error('error publishing provider record to peer - %e', err)
170+
})
171+
})
164172
})
165173
.catch(err => {
166174
events.end(err)

packages/kad-dht/src/query/manager.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,19 @@ export class QueryManager implements Startable {
172172
count: this.routingTable.kBucketSize
173173
})
174174

175-
// split peers into d buckets evenly(ish)
176-
const peersToQuery = peers.sort(() => {
177-
if (Math.random() > 0.5) {
178-
return 1
175+
// split peers into d buckets evenly(ish), preferring already-connected
176+
// peers to avoid opening new connections unnecessarily
177+
const peersToQuery = peers.sort((a, b) => {
178+
const aConnected = (this.connectionManager.getConnections(a) ?? []).length > 0 ? 0 : 1
179+
const bConnected = (this.connectionManager.getConnections(b) ?? []).length > 0 ? 0 : 1
180+
181+
if (aConnected !== bConnected) {
182+
// put connected peers first
183+
return aConnected - bConnected
179184
}
180185

181-
return -1
186+
// randomize within each group to maintain path diversity
187+
return Math.random() > 0.5 ? 1 : -1
182188
})
183189
.reduce((acc: PeerId[][], curr, index) => {
184190
acc[index % this.disjointPaths].push(curr)

packages/kad-dht/test/content-routing.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,34 @@ describe('content routing', () => {
354354
expect(sendMessageSpy.callCount).to.equal(initialMessageCalls,
355355
'No new network calls should be made after abort')
356356
})
357+
358+
it('queries connection status to prioritise connected peers when sending ADD_PROVIDER', async function () {
359+
this.timeout(20 * 1000)
360+
361+
const dhts = await sortDHTs(await Promise.all([
362+
testDHT.spawn(),
363+
testDHT.spawn(),
364+
testDHT.spawn(),
365+
testDHT.spawn()
366+
]), await kadUtils.convertBuffer(cid.multihash.bytes))
367+
368+
// connect all peers to the provider (dhts[3]) so they appear in its routing table
369+
await Promise.all([
370+
testDHT.connect(dhts[0], dhts[3]),
371+
testDHT.connect(dhts[1], dhts[3]),
372+
testDHT.connect(dhts[2], dhts[3])
373+
])
374+
375+
// getConnections is already a sinon stub from stubInterface — reset its history
376+
// and verify it gets called during provide() for the connected-peers-first sort
377+
dhts[3].components.connectionManager.getConnections.resetHistory()
378+
379+
await drain(dhts[3].dht.provide(cid))
380+
381+
// getConnections should have been called to check peer connection status
382+
// when sorting the closest peers for ADD_PROVIDER fan-out
383+
expect(dhts[3].components.connectionManager.getConnections.called).to.be.true(
384+
'getConnections should be called to prioritise connected peers during provide()'
385+
)
386+
})
357387
})

packages/kad-dht/test/query.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,4 +998,63 @@ describe('QueryManager', () => {
998998

999999
await manager.stop()
10001000
})
1001+
1002+
it('should prefer connected peers when distributing to disjoint paths', async () => {
1003+
// pick peers at higher XOR indices as "connected" and lower-XOR as "disconnected"
1004+
// so XOR-distance sorting alone would put disconnected peers first — only our
1005+
// connected-first bucket assignment can ensure each path gets a connected peer
1006+
const connectedPeers = peers.slice(10, 12).map(p => p.peerId)
1007+
const disconnectedPeers = peers.slice(0, 2).map(p => p.peerId)
1008+
// routing table returns the 4 peers in arbitrary order
1009+
const startingPeers = [...connectedPeers, ...disconnectedPeers]
1010+
1011+
const connectionManager = stubInterface<ConnectionManager>({
1012+
isDialable: async () => true
1013+
})
1014+
connectionManager.getConnections.callsFake((peerId?: PeerId) => {
1015+
if (peerId != null && connectedPeers.some(p => p.equals(peerId))) {
1016+
return [{}] as any
1017+
}
1018+
return []
1019+
})
1020+
1021+
const manager = new QueryManager({
1022+
peerId: ourPeerId,
1023+
logger: defaultLogger(),
1024+
connectionManager
1025+
}, {
1026+
...defaultInit(),
1027+
disjointPaths: 2,
1028+
alpha: 1
1029+
})
1030+
await manager.start()
1031+
1032+
routingTable.closestPeers.returns(startingPeers)
1033+
1034+
// track which peers are queried on each disjoint path
1035+
const peersByPath = new Map<number, PeerId[]>([[0, []], [1, []]])
1036+
1037+
const queryFunc: QueryFunc = async function * ({ peer, path }) {
1038+
peersByPath.get(path.index)?.push(peer.id)
1039+
1040+
yield peerResponseEvent({
1041+
from: peer.id,
1042+
messageType: MessageType.GET_VALUE,
1043+
path
1044+
})
1045+
}
1046+
1047+
await drain(manager.run(key, queryFunc))
1048+
1049+
// with connected-peers-first sort, even though disconnected peers have lower
1050+
// XOR distance, each disjoint path should get at least one connected starting peer
1051+
for (const [pathIndex, queriedPeers] of peersByPath) {
1052+
const hasConnectedPeer = queriedPeers.some(p => connectedPeers.some(c => c.equals(p)))
1053+
expect(hasConnectedPeer).to.be.true(
1054+
`path ${pathIndex} should have at least one connected starting peer`
1055+
)
1056+
}
1057+
1058+
await manager.stop()
1059+
})
10011060
})

0 commit comments

Comments
 (0)