forked from maidsafe/autonomi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(networking): add bootstrap cache for peer discovery
Add persistent bootstrap cache to maintain a list of previously known peers, improving network bootstrapping efficiency and reducing cold-start times.
- Loading branch information
Showing
11 changed files
with
2,132 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
[package] | ||
name = "bootstrap_cache" | ||
version = "0.1.0" | ||
edition = "2021" | ||
license = "GPL-3.0" | ||
authors = ["MaidSafe Developers <[email protected]>"] | ||
description = "Bootstrap cache functionality for the Safe Network" | ||
|
||
[dependencies] | ||
chrono = { version = "0.4", features = ["serde"] } | ||
dirs = "5.0" | ||
fs2 = "0.4.3" | ||
libp2p = { version = "0.53", features = ["serde"] } | ||
reqwest = { version = "0.11", features = ["json"] } | ||
serde = { version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" | ||
tempfile = "3.8.1" | ||
thiserror = "1.0" | ||
tokio = { version = "1.0", features = ["full", "sync"] } | ||
tracing = "0.1" | ||
|
||
[dev-dependencies] | ||
wiremock = "0.5" | ||
tokio = { version = "1.0", features = ["full", "test-util"] } | ||
tracing-subscriber = { version = "0.3", features = ["env-filter"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
# Bootstrap Cache | ||
|
||
A decentralized peer discovery and caching system for the Safe Network. | ||
|
||
## Features | ||
|
||
- **Decentralized Design**: No dedicated bootstrap nodes required | ||
- **Cross-Platform Support**: Works on Linux, macOS, and Windows | ||
- **Shared Cache**: System-wide cache file accessible by both nodes and clients | ||
- **Concurrent Access**: File locking for safe multi-process access | ||
- **Atomic Operations**: Safe cache updates using atomic file operations | ||
- **Initial Peer Discovery**: Fallback web endpoints for new/stale cache scenarios | ||
- **Comprehensive Error Handling**: Detailed error types and logging | ||
|
||
### Peer Management | ||
|
||
The bootstrap cache implements a robust peer management system: | ||
|
||
- **Peer Status Tracking**: Each peer's connection history is tracked, including: | ||
- Success count: Number of successful connections | ||
- Failure count: Number of failed connection attempts | ||
- Last seen timestamp: When the peer was last successfully contacted | ||
|
||
- **Automatic Cleanup**: The system automatically removes unreliable peers: | ||
- Peers that fail 3 consecutive connection attempts are marked for removal | ||
- Removal only occurs if there are at least 2 working peers available | ||
- This ensures network connectivity is maintained even during temporary connection issues | ||
|
||
- **Duplicate Prevention**: The cache automatically prevents duplicate peer entries: | ||
- Same IP and port combinations are only stored once | ||
- Different ports on the same IP are treated as separate peers | ||
|
||
## Installation | ||
|
||
Add this to your `Cargo.toml`: | ||
|
||
```toml | ||
[dependencies] | ||
bootstrap_cache = { version = "0.1.0" } | ||
``` | ||
|
||
## Usage | ||
|
||
### Basic Example | ||
|
||
```rust | ||
use bootstrap_cache::{BootstrapCache, CacheManager, InitialPeerDiscovery}; | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
// Initialize the cache manager | ||
let cache_manager = CacheManager::new()?; | ||
|
||
// Try to read from the cache | ||
let mut cache = match cache_manager.read_cache() { | ||
Ok(cache) if !cache.is_stale() => cache, | ||
_ => { | ||
// Cache is stale or unavailable, fetch initial peers | ||
let discovery = InitialPeerDiscovery::new(); | ||
let peers = discovery.fetch_peers().await?; | ||
let cache = BootstrapCache { | ||
last_updated: chrono::Utc::now(), | ||
peers, | ||
}; | ||
cache_manager.write_cache(&cache)?; | ||
cache | ||
} | ||
}; | ||
|
||
println!("Found {} peers in cache", cache.peers.len()); | ||
Ok(()) | ||
} | ||
``` | ||
|
||
### Custom Endpoints | ||
|
||
```rust | ||
use bootstrap_cache::InitialPeerDiscovery; | ||
|
||
let discovery = InitialPeerDiscovery::with_endpoints(vec![ | ||
"http://custom1.example.com/peers.json".to_string(), | ||
"http://custom2.example.com/peers.json".to_string(), | ||
]); | ||
``` | ||
|
||
### Peer Management Example | ||
|
||
```rust | ||
use bootstrap_cache::BootstrapCache; | ||
|
||
let mut cache = BootstrapCache::new(); | ||
|
||
// Add a new peer | ||
cache.add_peer("192.168.1.1".to_string(), 8080); | ||
|
||
// Update peer status after connection attempts | ||
cache.update_peer_status("192.168.1.1", 8080, true); // successful connection | ||
cache.update_peer_status("192.168.1.1", 8080, false); // failed connection | ||
|
||
// Clean up failed peers (only if we have at least 2 working peers) | ||
cache.cleanup_failed_peers(); | ||
``` | ||
|
||
## Cache File Location | ||
|
||
The cache file is stored in a system-wide location accessible to all processes: | ||
|
||
- **Linux**: `/var/safe/bootstrap_cache.json` | ||
- **macOS**: `/Library/Application Support/Safe/bootstrap_cache.json` | ||
- **Windows**: `C:\ProgramData\Safe\bootstrap_cache.json` | ||
|
||
## Cache File Format | ||
|
||
```json | ||
{ | ||
"last_updated": "2024-02-20T15:30:00Z", | ||
"peers": [ | ||
{ | ||
"ip": "192.168.1.1", | ||
"port": 8080, | ||
"last_seen": "2024-02-20T15:30:00Z", | ||
"success_count": 10, | ||
"failure_count": 0 | ||
} | ||
] | ||
} | ||
``` | ||
|
||
## Error Handling | ||
|
||
The crate provides detailed error types through the `Error` enum: | ||
|
||
```rust | ||
use bootstrap_cache::Error; | ||
|
||
match cache_manager.read_cache() { | ||
Ok(cache) => println!("Cache loaded successfully"), | ||
Err(Error::CacheStale) => println!("Cache is stale"), | ||
Err(Error::CacheCorrupted) => println!("Cache file is corrupted"), | ||
Err(Error::Io(e)) => println!("IO error: {}", e), | ||
Err(e) => println!("Other error: {}", e), | ||
} | ||
``` | ||
|
||
## Thread Safety | ||
|
||
The cache system uses file locking to ensure safe concurrent access: | ||
|
||
- Shared locks for reading | ||
- Exclusive locks for writing | ||
- Atomic file updates using temporary files | ||
|
||
## Development | ||
|
||
### Building | ||
|
||
```bash | ||
cargo build | ||
``` | ||
|
||
### Running Tests | ||
|
||
```bash | ||
cargo test | ||
``` | ||
|
||
### Running with Logging | ||
|
||
```rust | ||
use tracing_subscriber::FmtSubscriber; | ||
|
||
// Initialize logging | ||
let subscriber = FmtSubscriber::builder() | ||
.with_max_level(tracing::Level::DEBUG) | ||
.init(); | ||
``` | ||
|
||
## Contributing | ||
|
||
1. Fork the repository | ||
2. Create your feature branch (`git checkout -b feature/amazing-feature`) | ||
3. Commit your changes (`git commit -am 'Add amazing feature'`) | ||
4. Push to the branch (`git push origin feature/amazing-feature`) | ||
5. Open a Pull Request | ||
|
||
## License | ||
|
||
This project is licensed under the GPL-3.0 License - see the LICENSE file for details. | ||
|
||
## Related Documentation | ||
|
||
- [Bootstrap Cache PRD](docs/bootstrap_cache_prd.md) | ||
- [Implementation Guide](docs/bootstrap_cache_implementation.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
// Copyright 2024 MaidSafe.net limited. | ||
// | ||
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. | ||
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed | ||
// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. Please review the Licences for the specific language governing | ||
// permissions and limitations relating to use of the SAFE Network Software. | ||
|
||
use crate::{BootstrapCache, Error}; | ||
use fs2::FileExt; | ||
use std::{ | ||
fs::{self, File}, | ||
io::{self, Read, Write}, | ||
path::PathBuf, | ||
}; | ||
use tracing::{debug, error, info}; | ||
|
||
/// Manages reading and writing of the bootstrap cache file | ||
pub struct CacheManager { | ||
cache_path: PathBuf, | ||
} | ||
|
||
impl CacheManager { | ||
/// Creates a new CacheManager instance | ||
pub fn new() -> Result<Self, Error> { | ||
let cache_path = Self::get_cache_path()?; | ||
Ok(Self { cache_path }) | ||
} | ||
|
||
/// Returns the platform-specific cache file path | ||
fn get_cache_path() -> io::Result<PathBuf> { | ||
let path = if cfg!(target_os = "macos") { | ||
PathBuf::from("/Library/Application Support/Safe/bootstrap_cache.json") | ||
} else if cfg!(target_os = "linux") { | ||
PathBuf::from("/var/safe/bootstrap_cache.json") | ||
} else if cfg!(target_os = "windows") { | ||
PathBuf::from(r"C:\ProgramData\Safe\bootstrap_cache.json") | ||
} else { | ||
return Err(io::Error::new( | ||
io::ErrorKind::Other, | ||
"Unsupported operating system", | ||
)); | ||
}; | ||
|
||
if let Some(parent) = path.parent() { | ||
fs::create_dir_all(parent)?; | ||
} | ||
Ok(path) | ||
} | ||
|
||
/// Reads the cache file with file locking | ||
pub fn read_cache(&self) -> Result<BootstrapCache, Error> { | ||
debug!("Reading bootstrap cache from {:?}", self.cache_path); | ||
|
||
let mut file = match File::open(&self.cache_path) { | ||
Ok(file) => file, | ||
Err(e) if e.kind() == io::ErrorKind::NotFound => { | ||
info!("Cache file not found, creating new empty cache"); | ||
return Ok(BootstrapCache::new()); | ||
} | ||
Err(e) => { | ||
error!("Failed to open cache file: {}", e); | ||
return Err(e.into()); | ||
} | ||
}; | ||
|
||
// Acquire shared lock for reading | ||
file.lock_shared().map_err(|e| { | ||
error!("Failed to acquire shared lock: {}", e); | ||
Error::LockError | ||
})?; | ||
|
||
let mut contents = String::new(); | ||
file.read_to_string(&mut contents).map_err(|e| { | ||
error!("Failed to read cache file: {}", e); | ||
Error::Io(e) | ||
})?; | ||
|
||
// Release lock | ||
file.unlock().map_err(|e| { | ||
error!("Failed to release lock: {}", e); | ||
Error::LockError | ||
})?; | ||
|
||
serde_json::from_str(&contents).map_err(|e| { | ||
error!("Failed to parse cache file: {}", e); | ||
Error::Json(e) | ||
}) | ||
} | ||
|
||
/// Writes the cache file with file locking and atomic replacement | ||
pub fn write_cache(&self, cache: &BootstrapCache) -> Result<(), Error> { | ||
debug!("Writing bootstrap cache to {:?}", self.cache_path); | ||
|
||
let temp_path = self.cache_path.with_extension("tmp"); | ||
let mut file = File::create(&temp_path).map_err(|e| { | ||
error!("Failed to create temporary cache file: {}", e); | ||
Error::Io(e) | ||
})?; | ||
|
||
// Acquire exclusive lock for writing | ||
file.lock_exclusive().map_err(|e| { | ||
error!("Failed to acquire exclusive lock: {}", e); | ||
Error::LockError | ||
})?; | ||
|
||
let contents = serde_json::to_string_pretty(cache).map_err(|e| { | ||
error!("Failed to serialize cache: {}", e); | ||
Error::Json(e) | ||
})?; | ||
|
||
file.write_all(contents.as_bytes()).map_err(|e| { | ||
error!("Failed to write cache file: {}", e); | ||
Error::Io(e) | ||
})?; | ||
|
||
file.sync_all().map_err(|e| { | ||
error!("Failed to sync cache file: {}", e); | ||
Error::Io(e) | ||
})?; | ||
|
||
// Release lock | ||
file.unlock().map_err(|e| { | ||
error!("Failed to release lock: {}", e); | ||
Error::LockError | ||
})?; | ||
|
||
// Atomic rename | ||
fs::rename(&temp_path, &self.cache_path).map_err(|e| { | ||
error!("Failed to rename temporary cache file: {}", e); | ||
Error::Io(e) | ||
})?; | ||
|
||
info!("Successfully wrote cache file"); | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use chrono::Utc; | ||
use tempfile::tempdir; | ||
|
||
#[test] | ||
fn test_cache_read_write() { | ||
let dir = tempdir().unwrap(); | ||
let cache_path = dir.path().join("test_cache.json"); | ||
|
||
let cache = BootstrapCache { | ||
last_updated: Utc::now(), | ||
peers: vec![], | ||
}; | ||
|
||
let manager = CacheManager { cache_path }; | ||
manager.write_cache(&cache).unwrap(); | ||
|
||
let read_cache = manager.read_cache().unwrap(); | ||
assert_eq!(cache.peers.len(), read_cache.peers.len()); | ||
} | ||
|
||
#[test] | ||
fn test_missing_cache_file() { | ||
let dir = tempdir().unwrap(); | ||
let cache_path = dir.path().join("nonexistent.json"); | ||
|
||
let manager = CacheManager { cache_path }; | ||
let cache = manager.read_cache().unwrap(); | ||
assert!(cache.peers.is_empty()); | ||
} | ||
} |
Oops, something went wrong.