Skip to content

Commit

Permalink
feat(networking): add bootstrap cache for peer discovery
Browse files Browse the repository at this point in the history
Add persistent bootstrap cache to maintain a list of previously known peers,
improving network bootstrapping efficiency and reducing cold-start times.
  • Loading branch information
dirvine committed Nov 21, 2024
1 parent 3e7ed69 commit 8bef76d
Show file tree
Hide file tree
Showing 11 changed files with 2,132 additions and 0 deletions.
25 changes: 25 additions & 0 deletions bootstrap_cache/Cargo.toml
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"] }
193 changes: 193 additions & 0 deletions bootstrap_cache/README.md
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)
171 changes: 171 additions & 0 deletions bootstrap_cache/src/cache.rs
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());
}
}
Loading

0 comments on commit 8bef76d

Please sign in to comment.