use serde_json::json;
use sync_engine::{SyncEngine, SyncEngineConfig, SyncItem, EngineState};
use tokio::sync::watch;
use metrics_util::debugging::{DebugValue, DebuggingRecorder, Snapshotter};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
recorder.install().expect("failed to install metrics recorder");
tracing_subscriber::fmt()
.with_target(false)
.compact()
.init();
println!("\n╔═══════════════════════════════════════════════════════════════╗");
println!("║ sync-engine: Basic Usage Example ║");
println!("╚═══════════════════════════════════════════════════════════════╝\n");
println!("📦 Configuring sync-engine...");
let config = SyncEngineConfig {
redis_url: Some("redis://localhost:6379".into()),
sql_url: Some("mysql://test:test@localhost:3306/test".into()),
redis_prefix: Some("sync:".into()),
l1_max_bytes: 64 * 1024 * 1024,
batch_flush_count: 10,
batch_flush_ms: 500,
enable_cdc_stream: true,
cdc_stream_maxlen: 10_000,
..Default::default()
};
let (_config_tx, config_rx) = watch::channel(config.clone());
let mut engine = SyncEngine::new(config, config_rx);
println!(" State: {:?}", engine.state());
println!("\n🚀 Starting engine (connecting to backends)...");
engine.start().await?;
assert_eq!(engine.state(), EngineState::Ready);
println!(" ✅ Engine ready! State: {:?}", engine.state());
println!("\n📝 Creating 5 sample entries...");
println!(" ⏱️ Timing each .await() to showcase L1 cache speed");
let entries = vec![
("user.alice", json!({"name": "Alice", "role": "admin"})),
("user.bob", json!({"name": "Bob", "role": "user"})),
("user.carol", json!({"name": "Carol", "role": "user"})),
("config.app", json!({"theme": "dark", "version": "2.0"})),
("stats.daily", json!({"requests": 42000, "latency_p99": 12})),
];
let mut submit_times = Vec::new();
for (id, data) in &entries {
let item = SyncItem::from_json(id.to_string(), data.clone());
let start = std::time::Instant::now();
engine.submit(item).await?;
let elapsed = start.elapsed();
submit_times.push(elapsed);
println!(" └─ Submitted: {} → {} ({:?})", id, data, elapsed);
}
let avg_submit: std::time::Duration = submit_times.iter().sum::<std::time::Duration>() / submit_times.len() as u32;
println!(" ⚡ Submit avg: {:?} (L1 memory write, non-blocking)", avg_submit);
println!("\n⏳ Flushing to backends... (waiting 100ms for async writes)");
engine.force_flush().await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
println!(" ✅ Flush complete!");
println!("\n🔍 Checking existence...");
let def_missing = engine.definitely_missing("user.alice");
println!(" └─ definitely_missing('user.alice'): {}", def_missing);
let might = engine.might_exist("user.alice");
println!(" └─ might_exist('user.alice'): {}", might);
let exists = engine.contains("user.alice").await;
println!(" └─ contains('user.alice'): {}", exists);
let missing = engine.contains("user.nobody").await;
println!(" └─ contains('user.nobody'): {}", missing);
println!("\n📖 Fetching entries back (with timing)...");
println!(" ⏱️ These should hit L1 cache (microseconds!)");
let mut get_times = Vec::new();
for (id, _) in &entries {
let start = std::time::Instant::now();
let result = engine.get(id).await?;
let elapsed = start.elapsed();
get_times.push(elapsed);
match result {
Some(item) => {
let json = item.content_as_json()
.map(|v| v.to_string())
.unwrap_or_else(|| "<binary>".into());
println!(" └─ {} (v{}) → {} ({:?})", item.object_id, item.version, json, elapsed);
}
None => {
println!(" └─ {} → NOT FOUND ({:?})", id, elapsed);
}
}
}
let avg_get: std::time::Duration = get_times.iter().sum::<std::time::Duration>() / get_times.len() as u32;
println!(" ⚡ Get avg: {:?} (L1 cache hit)", avg_get);
println!("\n📊 Engine Metrics:");
let (l1_entries, l1_bytes) = engine.l1_stats();
println!(" ┌─ L1 Cache (Memory)");
println!(" │ └─ Entries: {}", l1_entries);
println!(" │ └─ Size: {} bytes", l1_bytes);
println!(" │ └─ Pressure: {:.1}%", engine.memory_pressure() * 100.0);
let (filter_entries, filter_capacity, trust) = engine.l3_filter_stats();
println!(" ├─ L3 Cuckoo Filter (before verify)");
println!(" │ └─ Entries: {}/{}", filter_entries, filter_capacity);
println!(" │ └─ Trust: {:?}", trust);
let (redis_root, sql_root) = engine.merkle_roots().await;
println!(" ├─ Merkle Trees");
println!(" │ └─ Redis (L2): {}", redis_root.as_deref().unwrap_or("(empty)"));
println!(" │ └─ SQL (L3): {}", sql_root.as_deref().unwrap_or("(empty)"));
if redis_root == sql_root {
println!(" │ └─ ✅ Roots match! Data is in sync.");
} else {
println!(" │ └─ ⚠️ Roots differ (expected during warmup)");
}
println!("\n🔐 Verifying L3 filter against SQL merkle...");
let trusted = engine.verify_filter().await;
let (_, _, trust_after) = engine.l3_filter_stats();
println!(" └─ Verified: {} → Trust: {:?}", trusted, trust_after);
println!("\n📍 Item Status:");
for (id, _) in &entries {
let status = engine.status(id).await;
println!(" └─ {}: {:?}", id, status);
}
println!("\n📈 Raw Metrics (OTEL export format):");
engine.update_gauge_metrics();
dump_metrics(&snapshotter);
println!("\n🛑 Shutting down...");
engine.shutdown().await;
println!(" ✅ Shutdown complete! State: {:?}", engine.state());
println!("\n🧹 Cleaning up WAL file...");
if let Err(e) = std::fs::remove_file("./sync_engine_wal.db") {
if e.kind() != std::io::ErrorKind::NotFound {
println!(" └─ Warning: could not remove WAL: {}", e);
}
} else {
println!(" └─ Removed: ./sync_engine_wal.db");
}
let _ = std::fs::remove_file("./sync_engine_wal.db-shm");
let _ = std::fs::remove_file("./sync_engine_wal.db-wal");
println!(" ✅ WAL cleanup complete!");
println!("\n💡 Data remains in Redis/MySQL - inspect with:");
println!(" └─ Redis: redis-cli JSON.GET sync:user.alice '$.payload'");
println!(" └─ MySQL: SELECT id, payload FROM sync_items;");
println!(" └─ Query: SELECT * FROM sync_items WHERE JSON_EXTRACT(payload, '$.name') = 'Alice';");
println!(" └─ UIs: http://localhost:5540 (RedisInsight)");
println!(" http://localhost:8080 (Adminer)");
println!("\n╔═══════════════════════════════════════════════════════════════╗");
println!("║ Example complete! ║");
println!("╚═══════════════════════════════════════════════════════════════╝\n");
Ok(())
}
fn dump_metrics(snapshotter: &Snapshotter) {
let snapshot = snapshotter.snapshot();
let mut counters: Vec<_> = vec![];
let mut gauges: Vec<_> = vec![];
let mut histograms: Vec<_> = vec![];
for (composite_key, _, _, value) in snapshot.into_vec() {
let (kind, key) = composite_key.into_parts();
let name = key.name();
let labels: Vec<_> = key.labels().map(|l| format!("{}={}", l.key(), l.value())).collect();
let label_str = if labels.is_empty() { String::new() } else { format!("{{{}}}", labels.join(",")) };
let _ = kind;
match value {
DebugValue::Counter(v) => counters.push((name.to_string(), label_str, v)),
DebugValue::Gauge(v) => gauges.push((name.to_string(), label_str, v.into_inner())),
DebugValue::Histogram(samples) => {
let count = samples.len();
let sum: f64 = samples.iter().map(|v| v.into_inner()).sum();
let avg = if count > 0 { sum / count as f64 } else { 0.0 };
let min = samples.iter().map(|v| v.into_inner()).fold(f64::INFINITY, f64::min);
let max = samples.iter().map(|v| v.into_inner()).fold(f64::NEG_INFINITY, f64::max);
histograms.push((name.to_string(), label_str, count, sum, avg, min, max));
}
}
}
counters.sort_by(|a, b| a.0.cmp(&b.0));
gauges.sort_by(|a, b| a.0.cmp(&b.0));
histograms.sort_by(|a, b| a.0.cmp(&b.0));
if !counters.is_empty() {
println!(" ┌─ Counters (cumulative)");
for (name, labels, value) in &counters {
println!(" │ └─ {}{} = {}", name, labels, value);
}
}
if !gauges.is_empty() {
println!(" ├─ Gauges (current value)");
for (name, labels, value) in &gauges {
println!(" │ └─ {}{} = {:.2}", name, labels, value);
}
}
if !histograms.is_empty() {
println!(" └─ Histograms (distributions)");
for (name, labels, count, sum, avg, min, max) in &histograms {
if *min == f64::INFINITY {
println!(" │ └─ {}{} = (no samples)", name, labels);
} else {
println!(" │ └─ {}{}", name, labels);
println!(" │ count={} sum={:.4} avg={:.4} min={:.4} max={:.4}",
count, sum, avg, min, max);
}
}
}
if counters.is_empty() && gauges.is_empty() && histograms.is_empty() {
println!(" └─ (no metrics recorded)");
}
}