stemedb/crates/stemedb-ingest/src/gossip.rs
jordan 2b0923f20e feat: Distributed replication foundation (Phase 6A) - HLC, Merkle trees, CRDT stores, sync protocol
- 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>
2026-02-02 19:31:54 -07:00

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());
}
}