- Add Hybrid Logical Clock (HLC) for causality tracking across nodes - Implement Merkle tree for efficient diff/sync with BLAKE3 hashing - Add CRDT-aware stores for assertions and votes with vector clocks - Create stemedb-sync crate with anti-entropy and gossip protocols - Add stemedb-rpc crate with gRPC sync service (proto definitions) - Implement SupersessionChain for tracking assertion lifecycles - Add Aphoria application for code analysis/reporting - Add battery11 replication test scaffolding - Fix .gitignore to exclude nested target directories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
3.5 KiB
Rust
130 lines
3.5 KiB
Rust
//! Gossip broadcast trait for distributed replication.
|
|
//!
|
|
//! This module defines the `GossipBroadcast` trait that allows the IngestWorker
|
|
//! to broadcast newly ingested assertions to peer nodes.
|
|
//!
|
|
//! # Design
|
|
//!
|
|
//! The trait is defined here in stemedb-ingest to avoid a cyclic dependency:
|
|
//! - stemedb-ingest needs the trait for IngestWorker
|
|
//! - stemedb-sync implements the trait (and depends on stemedb-ingest would cause cycle)
|
|
//!
|
|
//! By defining the trait here, stemedb-sync can implement it without the cycle.
|
|
|
|
use async_trait::async_trait;
|
|
use stemedb_core::types::HlcTimestamp;
|
|
use thiserror::Error;
|
|
|
|
/// Error type for gossip operations.
|
|
#[derive(Debug, Error)]
|
|
pub enum GossipError {
|
|
/// Network error during broadcast.
|
|
#[error("Network error: {0}")]
|
|
Network(String),
|
|
|
|
/// Serialization error.
|
|
#[error("Serialization error: {0}")]
|
|
Serialization(String),
|
|
|
|
/// All peers failed to receive the message.
|
|
#[error("All peers failed")]
|
|
AllPeersFailed,
|
|
}
|
|
|
|
/// Trait for broadcasting assertions to peer nodes.
|
|
///
|
|
/// Implementations should be:
|
|
/// - **Non-blocking**: Don't wait for all peers to acknowledge
|
|
/// - **Best-effort**: Log failures but don't block the ingestion pipeline
|
|
/// - **Idempotent-friendly**: Receivers handle duplicates gracefully
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// use stemedb_ingest::gossip::GossipBroadcast;
|
|
///
|
|
/// struct MyBroadcaster { /* ... */ }
|
|
///
|
|
/// #[async_trait]
|
|
/// impl GossipBroadcast for MyBroadcaster {
|
|
/// async fn broadcast(&self, hash: &[u8; 32], data: &[u8], hlc: &HlcTimestamp) -> Result<(), GossipError> {
|
|
/// // Send to peers...
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// fn is_enabled(&self) -> bool { true }
|
|
/// fn enable(&self) {}
|
|
/// fn disable(&self) {}
|
|
/// }
|
|
/// ```
|
|
#[async_trait]
|
|
pub trait GossipBroadcast: Send + Sync {
|
|
/// Broadcast an assertion to peer nodes.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `hash` - BLAKE3 hash of the assertion (32 bytes)
|
|
/// * `data` - Serialized assertion data (rkyv format)
|
|
/// * `hlc` - HLC timestamp for causal ordering
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `Ok(())` if at least one peer received the message, or if no peers
|
|
/// are configured. The method should not fail the ingestion pipeline.
|
|
async fn broadcast(
|
|
&self,
|
|
hash: &[u8; 32],
|
|
data: &[u8],
|
|
hlc: &HlcTimestamp,
|
|
) -> Result<(), GossipError>;
|
|
|
|
/// Check if broadcasting is currently enabled.
|
|
fn is_enabled(&self) -> bool;
|
|
|
|
/// Enable broadcasting.
|
|
fn enable(&self);
|
|
|
|
/// Disable broadcasting (e.g., for testing or during recovery).
|
|
fn disable(&self);
|
|
}
|
|
|
|
/// A no-op implementation for single-node deployments or testing.
|
|
pub struct NoOpGossipBroadcast;
|
|
|
|
#[async_trait]
|
|
impl GossipBroadcast for NoOpGossipBroadcast {
|
|
async fn broadcast(
|
|
&self,
|
|
_hash: &[u8; 32],
|
|
_data: &[u8],
|
|
_hlc: &HlcTimestamp,
|
|
) -> Result<(), GossipError> {
|
|
// Do nothing
|
|
Ok(())
|
|
}
|
|
|
|
fn is_enabled(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn enable(&self) {}
|
|
fn disable(&self) {}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_noop_broadcast() {
|
|
let broadcaster = NoOpGossipBroadcast;
|
|
let hash = [1u8; 32];
|
|
let data = vec![1, 2, 3];
|
|
let hlc = HlcTimestamp::new(1000, [1u8; 16]);
|
|
|
|
// Should always succeed
|
|
broadcaster.broadcast(&hash, &data, &hlc).await.expect("broadcast");
|
|
assert!(!broadcaster.is_enabled());
|
|
}
|
|
}
|