test: US-08 DWN sync tests pass 50/50 — fix sync performance

- Make dwn.sync endpoint async: spawns background task, returns immediately
- Add 90s overall timeout to sync_with_peers via tokio::time::timeout
- Deduplicate peer onion addresses before syncing
- Batch message pushes (50 per request) instead of one-at-a-time over Tor
- Add 15s connect_timeout to Tor SOCKS5 client
- Cap local message query to 200 messages per sync
- Fix DWN HTTP handler to process ALL messages in batch (was only first)
- Add recordId deduplication in handler to prevent duplicate imports
- Update test script to poll dwn.status for sync completion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-14 01:35:56 +00:00
co-authored by Claude Opus 4.6
parent a64d1b2d12
commit 65b5d5db8e
5 changed files with 371 additions and 116 deletions
+64 -29
View File
@@ -102,6 +102,7 @@ pub struct DwnStatusResponse {
/// and push our local messages, deduplicating by record_id.
pub async fn sync_with_peers(data_dir: &Path, peer_onions: &[String]) -> Result<DwnSyncState> {
use crate::network::dwn_store::{DwnStore, MessageQuery};
use std::collections::HashSet;
let mut state = load_sync_state(data_dir).await?;
state.status = SyncStatus::Syncing;
@@ -112,6 +113,7 @@ pub async fn sync_with_peers(data_dir: &Path, peer_onions: &[String]) -> Result<
let client = reqwest::Client::builder()
.proxy(socks_proxy)
.connect_timeout(std::time::Duration::from_secs(15))
.timeout(std::time::Duration::from_secs(30))
.build()
.context("Failed to build Tor HTTP client")?;
@@ -119,24 +121,47 @@ pub async fn sync_with_peers(data_dir: &Path, peer_onions: &[String]) -> Result<
let store = DwnStore::new(data_dir).await?;
let mut synced_count = 0u64;
// Get local messages since last sync (or all if first sync)
// Get local messages since last sync (or all if first sync, capped at 200)
let local_messages = store
.query_messages(&MessageQuery {
date_from: state.last_sync.clone(),
limit: Some(200),
..Default::default()
})
.await?;
for onion in peer_onions {
match sync_single_peer(&client, &store, onion, &local_messages, &state.last_sync).await {
Ok(count) => {
debug!(peer = %onion, messages = count, "Peer sync complete");
synced_count += count;
}
Err(e) => {
debug!(peer = %onion, error = %e, "Peer sync failed");
// Deduplicate peer onion addresses
let mut seen = HashSet::new();
let unique_onions: Vec<&String> = peer_onions
.iter()
.filter(|o| !o.is_empty() && seen.insert(o.as_str().to_string()))
.collect();
debug!(peers = unique_onions.len(), local_msgs = local_messages.len(), "Starting DWN sync");
// Overall sync timeout: 90 seconds
let sync_future = async {
for onion in &unique_onions {
match sync_single_peer(&client, &store, onion, &local_messages, &state.last_sync).await
{
Ok(count) => {
debug!(peer = %onion, messages = count, "Peer sync complete");
synced_count += count;
}
Err(e) => {
debug!(peer = %onion, error = %e, "Peer sync failed");
}
}
}
};
match tokio::time::timeout(std::time::Duration::from_secs(90), sync_future).await {
Ok(()) => {
debug!(count = synced_count, "DWN sync complete");
}
Err(_) => {
debug!("DWN sync timed out after 90s");
}
}
state.status = SyncStatus::Synced;
@@ -144,7 +169,6 @@ pub async fn sync_with_peers(data_dir: &Path, peer_onions: &[String]) -> Result<
state.messages_synced += synced_count;
save_sync_state(data_dir, &state).await?;
debug!(count = synced_count, "DWN sync complete");
Ok(state)
}
@@ -220,26 +244,37 @@ async fn sync_single_peer(
}
}
// Step 3: Push — send our local messages to the peer
for msg in local_messages {
let push_body = serde_json::json!({
"messages": [{
"descriptor": {
"interface": "Records",
"method": "Write",
"protocol": msg.descriptor.protocol,
"schema": msg.descriptor.schema,
"dataFormat": msg.descriptor.data_format,
},
"recordId": msg.record_id,
"author": msg.author,
"data": msg.data,
}]
});
// Step 3: Push — send local messages to peer in batches
let batch_size = 50;
for chunk in local_messages.chunks(batch_size) {
let messages: Vec<serde_json::Value> = chunk
.iter()
.map(|msg| {
serde_json::json!({
"descriptor": {
"interface": "Records",
"method": "Write",
"protocol": msg.descriptor.protocol,
"schema": msg.descriptor.schema,
"dataFormat": msg.descriptor.data_format,
},
"recordId": msg.record_id,
"author": msg.author,
"data": msg.data,
})
})
.collect();
// Best-effort push — don't fail the whole sync if one push fails
if let Err(e) = client.post(&dwn_url).json(&push_body).send().await {
debug!(record_id = %msg.record_id, error = %e, "Failed to push message to peer");
let push_body = serde_json::json!({ "messages": messages });
// Best-effort push — don't fail the whole sync if a batch fails
match client.post(&dwn_url).json(&push_body).send().await {
Ok(_) => {
debug!(count = chunk.len(), "Pushed message batch to peer");
}
Err(e) => {
debug!(error = %e, "Failed to push message batch to peer");
}
}
}