44import traceback
55from typing import Literal
66
7- from kubernetes .client import V1StatefulSet
8- from pydantic import BaseModel , ConfigDict , NonNegativeFloat , NonNegativeInt
7+ from kubernetes .client import V1Probe , V1ServicePort , V1StatefulSet , V1TCPSocketAction
8+ from pydantic import BaseModel , ConfigDict , NonNegativeFloat , NonNegativeInt , model_validator
99
10+ from src .deployments .core .builders import ServiceBuilder
1011from src .deployments .core .configs .container import Image
1112from src .deployments .experiments .base_experiment import BaseExperiment
1213from src .deployments .libp2p .bridge import Bridge
2223logger = logging .getLogger (__name__ )
2324
2425Muxer = Literal ["yamux" , "quic" , "mplex" ]
26+ Discovery = Literal ["static" , "kad-dht" ]
27+
28+ BOOTSTRAP_NAME = "bootstrap"
2529
2630
2731class ExpConfig (BaseModel ):
28- num_nodes : NonNegativeInt = 30
32+ num_relay_nodes : NonNegativeInt = 30
2933 num_messages : NonNegativeInt = 20
3034 message_size_bytes : NonNegativeInt = 1000
3135 delay_cold_start : NonNegativeFloat = 60
3236 delay_after_publish : NonNegativeFloat = 1
3337 muxer : Muxer = "yamux"
3438 image : Image = Image (repo = "pearsonwhite/dst-nimlibp2p-logging" , tag = "wip-4.2-1.16.0-amd" )
39+ # "kad-dht" discovers peers through a bootstrap node; "static" uses the CONNECTTO dial.
40+ discovery : Discovery = "static"
3541 connect_to : NonNegativeInt = 10
42+ bootstrap_nodes : NonNegativeInt = 1
3643 network_delay : NonNegativeInt = 0
3744 network_jitter : NonNegativeInt = 0
3845 node_start_delay : NonNegativeInt = 60
3946
47+ @model_validator (mode = "after" )
48+ def _check_bootstrap_nodes (self ):
49+ if self .discovery == "kad-dht" and self .bootstrap_nodes < 1 :
50+ raise ValueError ("kad-dht discovery requires bootstrap_nodes >= 1" )
51+ return self
52+
53+
54+ def bootstrap_dns (namespace : str ) -> str :
55+ return f"{ BOOTSTRAP_NAME } .{ namespace } .svc.cluster.local"
56+
4057
4158def build_nodes (
4259 namespace : str ,
@@ -47,18 +64,26 @@ def build_nodes(
4764 .with_libp2p_config (
4865 name = "pod" ,
4966 namespace = namespace ,
50- num_nodes = params .num_nodes ,
67+ num_nodes = params .num_relay_nodes ,
5168 dns_searches = ["nimp2p-service" ],
5269 )
53- .with_option (NimLibp2p .peers , params .num_nodes )
70+ .with_option (NimLibp2p .peers , params .num_relay_nodes )
5471 .with_option (NimLibp2p .self_trigger , True )
55- .with_option (NimLibp2p .service , "nimp2p-service" )
5672 .with_option (NimLibp2p .muxer , params .muxer )
57- .with_option (NimLibp2p .connect_to , params .connect_to )
5873 .with_option (NimLibp2p .cold_start_delay , params .node_start_delay )
5974 .with_readiness_probe (readiness_probe_metrics ())
6075 .with_image (params .image )
6176 )
77+ if params .discovery == "kad-dht" :
78+ builder = (
79+ builder .with_option (NimLibp2p .node_role , "RoleNormal" )
80+ .with_option (NimLibp2p .discovery , "kad-dht" )
81+ .with_option (NimLibp2p .service , bootstrap_dns (namespace ))
82+ )
83+ else :
84+ builder = builder .with_option (NimLibp2p .service , "nimp2p-service" ).with_option (
85+ NimLibp2p .connect_to , params .connect_to
86+ )
6287 if params .network_delay or params .network_jitter :
6388 builder = builder .with_network_delay (
6489 delay = params .network_delay , jitter = params .network_jitter
@@ -67,6 +92,45 @@ def build_nodes(
6792 return builder .build ()
6893
6994
95+ def build_bootstrap_service (namespace : str ):
96+ return (
97+ ServiceBuilder ()
98+ .with_name (BOOTSTRAP_NAME )
99+ .with_namespace (namespace )
100+ .with_cluster_ip ("None" )
101+ .with_selector ("app" , "zerotenkay" )
102+ .with_selector ("role" , "bootstrap" )
103+ .with_port (V1ServicePort (name = "p2p" , port = 5000 , target_port = 5000 ))
104+ .build ()
105+ )
106+
107+
108+ def build_bootstrap_nodes (namespace : str , params : ExpConfig ) -> V1StatefulSet :
109+ # Every node holds a link to the anchor, so it must accept more than num_relay_nodes.
110+ # Probe the metrics port (always TCP, unlike the p2p port under quic).
111+ return (
112+ Libp2pStatefulSetBuilder ()
113+ .with_libp2p_config (
114+ name = BOOTSTRAP_NAME , namespace = namespace , num_nodes = params .bootstrap_nodes
115+ )
116+ .with_label ("role" , "bootstrap" )
117+ .with_option (NimLibp2p .node_role , "RoleBootstrap" )
118+ .with_option (NimLibp2p .discovery , "kad-dht" )
119+ .with_option (NimLibp2p .muxer , params .muxer )
120+ .with_option (NimLibp2p .max_connections , params .num_relay_nodes + 100 )
121+ .with_readiness_probe (
122+ V1Probe (
123+ tcp_socket = V1TCPSocketAction (port = 8008 ),
124+ initial_delay_seconds = 5 ,
125+ period_seconds = 2 ,
126+ failure_threshold = 3 ,
127+ )
128+ )
129+ .with_image (params .image )
130+ .build ()
131+ )
132+
133+
70134async def publish (config , namespace , random_name ):
71135 try :
72136 target = Target (
@@ -107,6 +171,17 @@ async def _run(self):
107171 )
108172 await self .deploy (deployment = publisher , wait_for_ready = True )
109173
174+ # Bootstrap (kad-dht only): anchor node + headless discovery service. Deployed
175+ # before the nodes so the mesh can form through it once the nodes wake up.
176+ if self .config .discovery == "kad-dht" :
177+ bootstrap_service = build_bootstrap_service (self .namespace )
178+ self .dump_yaml (bootstrap_service , "bootstrap-service" )
179+ await self .deploy (deployment = bootstrap_service )
180+
181+ bootstrap = build_bootstrap_nodes (namespace = self .namespace , params = self .config )
182+ self .dump_yaml (bootstrap , "bootstrap" )
183+ await self .deploy (deployment = bootstrap , wait_for_ready = True )
184+
110185 # Nodes
111186 nodes = build_nodes (
112187 namespace = self .namespace ,
@@ -125,7 +200,7 @@ async def _run(self):
125200
126201 tasks = []
127202 for msg_index in range (self .config .num_messages ):
128- index = random .randint (0 , self .config .num_nodes - 1 )
203+ index = random .randint (0 , self .config .num_relay_nodes - 1 )
129204 random_name = f"{ name } -{ index } "
130205 self .log_event ({"event" : "publish" , "node" : random_name , "index" : msg_index })
131206 tasks .append (asyncio .create_task (publish (self .config , namespace , random_name )))
0 commit comments