Skip to content

Commit 7a4c955

Browse files
authored
Merge branch 'main' into fix/issue-883-transport-issues-todos
2 parents d620270 + 970b535 commit 7a4c955

24 files changed

+1625
-426
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,6 @@ env.bak/
178178
#lockfiles
179179
uv.lock
180180
poetry.lock
181+
182+
# Sphinx documentation build
183+
_build/
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
Multiple Connections Per Peer
2+
=============================
3+
4+
This example demonstrates how to use the multiple connections per peer feature in py-libp2p.
5+
6+
Overview
7+
--------
8+
9+
The multiple connections per peer feature allows a libp2p node to maintain multiple network connections to the same peer. This provides several benefits:
10+
11+
- **Improved reliability**: If one connection fails, others remain available
12+
- **Better performance**: Load can be distributed across multiple connections
13+
- **Enhanced throughput**: Multiple streams can be created in parallel
14+
- **Fault tolerance**: Redundant connections provide backup paths
15+
16+
Configuration
17+
-------------
18+
19+
The feature is configured through the `ConnectionConfig` class:
20+
21+
.. code-block:: python
22+
23+
from libp2p.network.swarm import ConnectionConfig
24+
25+
# Default configuration
26+
config = ConnectionConfig()
27+
print(f"Max connections per peer: {config.max_connections_per_peer}")
28+
print(f"Load balancing strategy: {config.load_balancing_strategy}")
29+
30+
# Custom configuration
31+
custom_config = ConnectionConfig(
32+
max_connections_per_peer=5,
33+
connection_timeout=60.0,
34+
load_balancing_strategy="least_loaded"
35+
)
36+
37+
Load Balancing Strategies
38+
-------------------------
39+
40+
Two load balancing strategies are available:
41+
42+
**Round Robin** (default)
43+
Cycles through connections in order, distributing load evenly.
44+
45+
**Least Loaded**
46+
Selects the connection with the fewest active streams.
47+
48+
API Usage
49+
---------
50+
51+
The new API provides direct access to multiple connections:
52+
53+
.. code-block:: python
54+
55+
from libp2p import new_swarm
56+
57+
# Create swarm with multiple connections support
58+
swarm = new_swarm()
59+
60+
# Dial a peer - returns list of connections
61+
connections = await swarm.dial_peer(peer_id)
62+
print(f"Established {len(connections)} connections")
63+
64+
# Get all connections to a peer
65+
peer_connections = swarm.get_connections(peer_id)
66+
67+
# Get all connections (across all peers)
68+
all_connections = swarm.get_connections()
69+
70+
# Get the complete connections map
71+
connections_map = swarm.get_connections_map()
72+
73+
# Backward compatibility - get single connection
74+
single_conn = swarm.get_connection(peer_id)
75+
76+
Backward Compatibility
77+
----------------------
78+
79+
Existing code continues to work through backward compatibility features:
80+
81+
.. code-block:: python
82+
83+
# Legacy 1:1 mapping (returns first connection for each peer)
84+
legacy_connections = swarm.connections_legacy
85+
86+
# Single connection access (returns first available connection)
87+
conn = swarm.get_connection(peer_id)
88+
89+
Example
90+
-------
91+
92+
A complete working example is available in the `examples/doc-examples/multiple_connections_example.py` file.
93+
94+
Production Configuration
95+
-------------------------
96+
97+
For production use, consider these settings:
98+
99+
**RetryConfig Parameters**
100+
101+
The `RetryConfig` class controls connection retry behavior with exponential backoff:
102+
103+
- **max_retries**: Maximum number of retry attempts before giving up (default: 3)
104+
- **initial_delay**: Initial delay in seconds before the first retry (default: 0.1s)
105+
- **max_delay**: Maximum delay cap to prevent excessive wait times (default: 30.0s)
106+
- **backoff_multiplier**: Exponential backoff multiplier - each retry multiplies delay by this factor (default: 2.0)
107+
- **jitter_factor**: Random jitter (0.0-1.0) to prevent synchronized retries (default: 0.1)
108+
109+
**ConnectionConfig Parameters**
110+
111+
The `ConnectionConfig` class manages multi-connection behavior:
112+
113+
- **max_connections_per_peer**: Maximum connections allowed to a single peer (default: 3)
114+
- **connection_timeout**: Timeout for establishing new connections in seconds (default: 30.0s)
115+
- **load_balancing_strategy**: Strategy for distributing streams ("round_robin" or "least_loaded")
116+
117+
**Load Balancing Strategies Explained**
118+
119+
- **round_robin**: Cycles through connections in order, distributing load evenly. Simple and predictable.
120+
- **least_loaded**: Selects the connection with the fewest active streams. Better for performance but more complex.
121+
122+
.. code-block:: python
123+
124+
from libp2p.network.swarm import ConnectionConfig, RetryConfig
125+
126+
# Production-ready configuration
127+
retry_config = RetryConfig(
128+
max_retries=3, # Maximum retry attempts before giving up
129+
initial_delay=0.1, # Start with 100ms delay
130+
max_delay=30.0, # Cap exponential backoff at 30 seconds
131+
backoff_multiplier=2.0, # Double delay each retry (100ms -> 200ms -> 400ms)
132+
jitter_factor=0.1 # Add 10% random jitter to prevent thundering herd
133+
)
134+
135+
connection_config = ConnectionConfig(
136+
max_connections_per_peer=3, # Allow up to 3 connections per peer
137+
connection_timeout=30.0, # 30 second timeout for new connections
138+
load_balancing_strategy="round_robin" # Simple, predictable load distribution
139+
)
140+
141+
swarm = new_swarm(
142+
retry_config=retry_config,
143+
connection_config=connection_config
144+
)
145+
146+
**How RetryConfig Works in Practice**
147+
148+
With the configuration above, connection retries follow this pattern:
149+
150+
1. **Attempt 1**: Immediate connection attempt
151+
2. **Attempt 2**: Wait 100ms ± 10ms jitter, then retry
152+
3. **Attempt 3**: Wait 200ms ± 20ms jitter, then retry
153+
4. **Attempt 4**: Wait 400ms ± 40ms jitter, then retry
154+
5. **Attempt 5**: Wait 800ms ± 80ms jitter, then retry
155+
6. **Attempt 6**: Wait 1.6s ± 160ms jitter, then retry
156+
7. **Attempt 7**: Wait 3.2s ± 320ms jitter, then retry
157+
8. **Attempt 8**: Wait 6.4s ± 640ms jitter, then retry
158+
9. **Attempt 9**: Wait 12.8s ± 1.28s jitter, then retry
159+
10. **Attempt 10**: Wait 25.6s ± 2.56s jitter, then retry
160+
11. **Attempt 11**: Wait 30.0s (capped) ± 3.0s jitter, then retry
161+
12. **Attempt 12**: Wait 30.0s (capped) ± 3.0s jitter, then retry
162+
13. **Give up**: After 12 retries (3 initial + 9 retries), connection fails
163+
164+
The jitter prevents multiple clients from retrying simultaneously, reducing server load.
165+
166+
**Parameter Tuning Guidelines**
167+
168+
**For Development/Testing:**
169+
- Use lower `max_retries` (1-2) and shorter delays for faster feedback
170+
- Example: `RetryConfig(max_retries=2, initial_delay=0.01, max_delay=0.1)`
171+
172+
**For Production:**
173+
- Use moderate `max_retries` (3-5) with reasonable delays for reliability
174+
- Example: `RetryConfig(max_retries=5, initial_delay=0.1, max_delay=60.0)`
175+
176+
**For High-Latency Networks:**
177+
- Use higher `max_retries` (5-10) with longer delays
178+
- Example: `RetryConfig(max_retries=8, initial_delay=0.5, max_delay=120.0)`
179+
180+
**For Load Balancing:**
181+
- Use `round_robin` for simple, predictable behavior
182+
- Use `least_loaded` when you need optimal performance and can handle complexity
183+
184+
Architecture
185+
------------
186+
187+
The implementation follows the same architectural patterns as the Go and JavaScript reference implementations:
188+
189+
- **Core data structure**: `dict[ID, list[INetConn]]` for 1:many mapping
190+
- **API consistency**: Methods like `get_connections()` match reference implementations
191+
- **Load balancing**: Integrated at the API level for optimal performance
192+
- **Backward compatibility**: Maintains existing interfaces for gradual migration
193+
194+
This design ensures consistency across libp2p implementations while providing the benefits of multiple connections per peer.

docs/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Examples
1515
examples.kademlia
1616
examples.mDNS
1717
examples.random_walk
18+
examples.multiple_connections
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating multiple connections per peer support in libp2p.
4+
5+
This example shows how to:
6+
1. Configure multiple connections per peer
7+
2. Use different load balancing strategies
8+
3. Access multiple connections through the new API
9+
4. Maintain backward compatibility
10+
"""
11+
12+
import logging
13+
14+
import trio
15+
16+
from libp2p import new_swarm
17+
from libp2p.network.swarm import ConnectionConfig, RetryConfig
18+
19+
# Set up logging
20+
logging.basicConfig(level=logging.INFO)
21+
logger = logging.getLogger(__name__)
22+
23+
24+
async def example_basic_multiple_connections() -> None:
25+
"""Example of basic multiple connections per peer usage."""
26+
logger.info("Creating swarm with multiple connections support...")
27+
28+
# Create swarm with default configuration
29+
swarm = new_swarm()
30+
default_connection = ConnectionConfig()
31+
32+
logger.info(f"Swarm created with peer ID: {swarm.get_peer_id()}")
33+
logger.info(
34+
f"Connection config: max_connections_per_peer="
35+
f"{default_connection.max_connections_per_peer}"
36+
)
37+
38+
await swarm.close()
39+
logger.info("Basic multiple connections example completed")
40+
41+
42+
async def example_custom_connection_config() -> None:
43+
"""Example of custom connection configuration."""
44+
logger.info("Creating swarm with custom connection configuration...")
45+
46+
# Custom connection configuration for high-performance scenarios
47+
connection_config = ConnectionConfig(
48+
max_connections_per_peer=5, # More connections per peer
49+
connection_timeout=60.0, # Longer timeout
50+
load_balancing_strategy="least_loaded", # Use least loaded strategy
51+
)
52+
53+
# Create swarm with custom connection config
54+
swarm = new_swarm(connection_config=connection_config)
55+
56+
logger.info("Custom connection config applied:")
57+
logger.info(
58+
f" Max connections per peer: {connection_config.max_connections_per_peer}"
59+
)
60+
logger.info(f" Connection timeout: {connection_config.connection_timeout}s")
61+
logger.info(
62+
f" Load balancing strategy: {connection_config.load_balancing_strategy}"
63+
)
64+
65+
await swarm.close()
66+
logger.info("Custom connection config example completed")
67+
68+
69+
async def example_multiple_connections_api() -> None:
70+
"""Example of using the new multiple connections API."""
71+
logger.info("Demonstrating multiple connections API...")
72+
73+
connection_config = ConnectionConfig(
74+
max_connections_per_peer=3, load_balancing_strategy="round_robin"
75+
)
76+
77+
swarm = new_swarm(connection_config=connection_config)
78+
79+
logger.info("Multiple connections API features:")
80+
logger.info(" - dial_peer() returns list[INetConn]")
81+
logger.info(" - get_connections(peer_id) returns list[INetConn]")
82+
logger.info(" - get_connections_map() returns dict[ID, list[INetConn]]")
83+
logger.info(
84+
" - get_connection(peer_id) returns INetConn | None (backward compatibility)"
85+
)
86+
87+
await swarm.close()
88+
logger.info("Multiple connections API example completed")
89+
90+
91+
async def example_backward_compatibility() -> None:
92+
"""Example of backward compatibility features."""
93+
logger.info("Demonstrating backward compatibility...")
94+
95+
swarm = new_swarm()
96+
97+
logger.info("Backward compatibility features:")
98+
logger.info(" - connections_legacy property provides 1:1 mapping")
99+
logger.info(" - get_connection() method for single connection access")
100+
logger.info(" - Existing code continues to work")
101+
102+
await swarm.close()
103+
logger.info("Backward compatibility example completed")
104+
105+
106+
async def example_production_ready_config() -> None:
107+
"""Example of production-ready configuration."""
108+
logger.info("Creating swarm with production-ready configuration...")
109+
110+
# Production-ready retry configuration
111+
retry_config = RetryConfig(
112+
max_retries=3, # Reasonable retry limit
113+
initial_delay=0.1, # Quick initial retry
114+
max_delay=30.0, # Cap exponential backoff
115+
backoff_multiplier=2.0, # Standard exponential backoff
116+
jitter_factor=0.1, # Small jitter to prevent thundering herd
117+
)
118+
119+
# Production-ready connection configuration
120+
connection_config = ConnectionConfig(
121+
max_connections_per_peer=3, # Balance between performance and resource usage
122+
connection_timeout=30.0, # Reasonable timeout
123+
load_balancing_strategy="round_robin", # Simple, predictable strategy
124+
)
125+
126+
# Create swarm with production config
127+
swarm = new_swarm(retry_config=retry_config, connection_config=connection_config)
128+
129+
logger.info("Production-ready configuration applied:")
130+
logger.info(
131+
f" Retry: {retry_config.max_retries} retries, "
132+
f"{retry_config.max_delay}s max delay"
133+
)
134+
logger.info(f" Connections: {connection_config.max_connections_per_peer} per peer")
135+
logger.info(f" Load balancing: {connection_config.load_balancing_strategy}")
136+
137+
await swarm.close()
138+
logger.info("Production-ready configuration example completed")
139+
140+
141+
async def main() -> None:
142+
"""Run all examples."""
143+
logger.info("Multiple Connections Per Peer Examples")
144+
logger.info("=" * 50)
145+
146+
try:
147+
await example_basic_multiple_connections()
148+
logger.info("-" * 30)
149+
150+
await example_custom_connection_config()
151+
logger.info("-" * 30)
152+
153+
await example_multiple_connections_api()
154+
logger.info("-" * 30)
155+
156+
await example_backward_compatibility()
157+
logger.info("-" * 30)
158+
159+
await example_production_ready_config()
160+
logger.info("-" * 30)
161+
162+
logger.info("All examples completed successfully!")
163+
164+
except Exception as e:
165+
logger.error(f"Example failed: {e}")
166+
raise
167+
168+
169+
if __name__ == "__main__":
170+
trio.run(main)

0 commit comments

Comments
 (0)