Skip to content

Commit af9651a

Browse files
SionoiSvladopajic
andauthored
feat(service-disco): service table bootstrapping (#2718)
Co-authored-by: vladopajic <vladopajic@users.noreply.github.com>
1 parent bd68993 commit af9651a

4 files changed

Lines changed: 77 additions & 1 deletion

File tree

libp2p/protocols/service_discovery.nim

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (c) Status Research & Development GmbH
33

44
import chronos, chronicles, results, sets
5-
import ../utils/heartbeat
5+
import ../utils/[heartbeat, future]
66
import ../[peerid, switch, multihash, peerinfo, extended_peer_record]
77
import ./kademlia
88
import
@@ -50,6 +50,15 @@ proc maintainServiceTables(
5050
disco.config.bucketRefreshTime, sleepFirst = true:
5151
await disco.rtManager.refreshAllTables(disco)
5252

53+
proc bootstrapServiceTable*(
54+
disco: ServiceDiscovery, serviceId: ServiceId
55+
) {.async: (raises: [CancelledError]).} =
56+
let rtable = disco.rtManager.getTable(serviceId).valueOr:
57+
return
58+
59+
await disco.refreshTable(rtable, forceRefresh = true)
60+
debug "Service table bootstrap complete", serviceId
61+
5362
proc new*(
5463
T: typedesc[ServiceDiscovery],
5564
switch: Switch,
@@ -87,6 +96,12 @@ proc new*(
8796
# Fill up buckets with initial bootstrap nodes
8897
disco.updatePeers(bootstrapNodes)
8998

99+
disco.rtManager.onServiceTableCreated = proc(serviceId: ServiceId) =
100+
if disco.config.disableBootstrapping:
101+
return
102+
103+
disco.serviceBootstrapFuts.trackFut(disco.bootstrapServiceTable(serviceId))
104+
90105
disco.codec = codec
91106
if client:
92107
return disco
@@ -158,6 +173,9 @@ method stop*(disco: ServiceDiscovery) {.async: (raises: []).} =
158173

159174
disco.advertiser.clear()
160175

176+
disco.serviceBootstrapFuts.cancelSoon()
177+
disco.serviceBootstrapFuts = @[]
178+
161179
if not disco.selfSignedPeerRecordLoop.isNil():
162180
disco.selfSignedPeerRecordLoop.cancelSoon()
163181
disco.selfSignedPeerRecordLoop = nil

libp2p/protocols/service_discovery/routing_table_manager.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ proc addService*(
6161
manager.serviceStatus[serviceId] = status
6262

6363
manager.updateServiceTablesMetrics()
64+
65+
if not manager.onServiceTableCreated.isNil:
66+
manager.onServiceTableCreated(serviceId)
67+
6468
return true
6569

6670
proc removeService*(

libp2p/protocols/service_discovery/types.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type
4343
ServiceRoutingTableManager* = ref object
4444
tables*: Table[ServiceId, RoutingTable]
4545
serviceStatus*: Table[ServiceId, ServiceStatus]
46+
onServiceTableCreated*: proc(serviceId: ServiceId) {.gcsafe, closure, raises: [].}
4647

4748
AdvertisementKey* = tuple[peerId: PeerId, seqNo: uint64]
4849

@@ -94,6 +95,7 @@ type
9495
refreshServiceTablesLoop*: Future[void]
9596
advertiserMaintenanceLoop*: Future[void]
9697
localRegistrationLoop*: Future[void]
98+
serviceBootstrapFuts*: seq[Future[void]]
9799
clientMode*: bool
98100

99101
proc new*(

tests/libp2p/service_discovery/test_routing_table_manager.nim

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ proc makeKey(x: byte): Key =
1818
proc makeServiceId(x: byte): ServiceId =
1919
return makeKey(x)
2020

21+
# Heap-allocated recorder so a closure capturing it stays GC-safe (mirrors how
22+
# the production callback captures the disco ref rather than a stack local).
23+
type HitRecorder = ref object
24+
ids: seq[ServiceId]
25+
2126
proc makeMainTable(selfId: Key, peers: seq[Key]): RoutingTable =
2227
var rt = RoutingTable.new(selfId)
2328
for p in peers:
@@ -75,6 +80,53 @@ suite "ServiceRoutingTableManager":
7580
upgraded == true
7681
manager.serviceStatus[serviceId] == Both
7782

83+
test "addService fires onServiceTableCreated only for brand-new tables":
84+
let manager = ServiceRoutingTableManager.new()
85+
let mainRt = RoutingTable.new(makeKey(0))
86+
87+
let hits = HitRecorder()
88+
manager.onServiceTableCreated = proc(sid: ServiceId) =
89+
hits.ids.add(sid)
90+
91+
let serviceId = makeServiceId(1)
92+
93+
check manager.addService(
94+
serviceId, mainRt, DefaultReplication, DefaultMaxBuckets, Interest
95+
)
96+
check:
97+
hits.ids.len == 1
98+
hits.ids[0] == serviceId
99+
100+
discard manager.addService(
101+
serviceId, mainRt, DefaultReplication, DefaultMaxBuckets, Interest
102+
)
103+
check hits.ids.len == 1
104+
105+
discard manager.addService(
106+
serviceId, mainRt, DefaultReplication, DefaultMaxBuckets, Provided
107+
)
108+
check:
109+
hits.ids.len == 1
110+
manager.serviceStatus[serviceId] == Both
111+
112+
let otherId = makeServiceId(2)
113+
check manager.addService(
114+
otherId, mainRt, DefaultReplication, DefaultMaxBuckets, Provided
115+
)
116+
check:
117+
hits.ids.len == 2
118+
otherId in hits.ids
119+
120+
test "addService with no callback set does not crash":
121+
let manager = ServiceRoutingTableManager.new()
122+
let serviceId = makeServiceId(1)
123+
let mainRt = RoutingTable.new(makeKey(0))
124+
125+
check manager.addService(
126+
serviceId, mainRt, DefaultReplication, DefaultMaxBuckets, Interest
127+
)
128+
check manager.hasService(serviceId)
129+
78130
test "addService pre-populates table from main routing table":
79131
let selfId = makeKey(0)
80132
let peer1 = makeKey(1)

0 commit comments

Comments
 (0)