use crate::{
agents::{Agent, ForAgent},
client::ws::{WsClient, WsMessage},
db::{trees::Tree, Db},
errors::{AtomicError, AtomicResult},
sync::{engine, protocol},
Storelike,
};
use tokio::sync::broadcast::Receiver;
pub enum ReplicateAuth {
Agent(Box<Agent>),
PreSigned(Vec<u8>),
Anonymous,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplicateOutcome {
pub pushed: usize,
pub blobs_served: usize,
pub in_sync: bool,
}
const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const TOTAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60);
pub async fn replicate_drive_to_remote(
store: &Db,
drive: &str,
target_ws_url: &str,
export_as: &ForAgent,
auth: ReplicateAuth,
) -> AtomicResult<ReplicateOutcome> {
let started = std::time::Instant::now();
let client = WsClient::connect(target_ws_url).await?;
match auth {
ReplicateAuth::Agent(agent) => client.authenticate(&agent).await?,
ReplicateAuth::PreSigned(frame) => client.authenticate_with_frame(frame).await?,
ReplicateAuth::Anonymous => {}
}
let mut rx = client.subscribe();
let mut outcome = ReplicateOutcome {
pushed: 0,
blobs_served: 0,
in_sync: false,
};
client
.send_binary(build_sync_frame(store, drive).await)
.await?;
let sent = drive_exchange(
&client,
&mut rx,
store,
drive,
export_as,
&mut outcome,
started,
)
.await?;
if sent == 0 && outcome.in_sync {
return Ok(outcome);
}
outcome.in_sync = false;
client
.send_binary(build_sync_frame(store, drive).await)
.await?;
drive_exchange(
&client,
&mut rx,
store,
drive,
export_as,
&mut outcome,
started,
)
.await?;
Ok(outcome)
}
async fn drive_exchange(
client: &WsClient,
rx: &mut Receiver<WsMessage>,
store: &Db,
drive: &str,
export_as: &ForAgent,
outcome: &mut ReplicateOutcome,
started: std::time::Instant,
) -> AtomicResult<usize> {
let mut sent_this_round = 0;
loop {
if started.elapsed() > TOTAL_TIMEOUT {
return Err(AtomicError::from(format!(
"Replicating {drive} exceeded the time limit"
)));
}
let msg = match tokio::time::timeout(IDLE_TIMEOUT, rx.recv()).await {
Err(_) | Ok(Err(_)) => break,
Ok(Ok(msg)) => msg,
};
match msg {
WsMessage::SyncOk { drive: d } if d == drive && sent_this_round == 0 => {
outcome.in_sync = true;
break;
}
WsMessage::SyncDiff { drive: d, pull, .. } if d == drive => {
if pull.is_empty() {
break;
}
let entries = engine::collect_readable_snapshots(store, export_as, &pull).await;
if entries.is_empty() {
tracing::warn!(
"[replicate] remote asked for {} subjects of {drive} but none are readable by {export_as:?}",
pull.len()
);
break;
}
let refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(s, b)| (s.as_str(), b.as_slice()))
.collect();
for chunk in protocol::encode_sync_push_chunks(drive, &refs) {
client.send_binary(chunk).await?;
}
sent_this_round += entries.len();
outcome.pushed += entries.len();
tracing::info!("[replicate] pushed {} resources of {drive}", entries.len());
}
WsMessage::BlobRequest { hash } => {
match store.kv.get(Tree::Blobs, &hash) {
Ok(Some(bytes)) => {
client
.send_binary(protocol::encode_blob_response(&hash, &bytes))
.await?;
outcome.blobs_served += 1;
}
_ => tracing::warn!("[replicate] remote asked for a blob we don't have"),
}
}
WsMessage::SyncPush { .. } => {}
WsMessage::Error(e) => {
return Err(AtomicError::from(format!(
"Remote refused to sync {drive}: {e}"
)));
}
_ => {}
}
}
Ok(sent_this_round)
}
async fn build_sync_frame(store: &Db, drive: &str) -> Vec<u8> {
let drive_subject = crate::Subject::from_raw(drive, store.get_base_domain().as_deref());
let subjects = engine::collect_drive_subjects(store, &drive_subject).await;
let vvs = engine::build_drive_vvs(store, &subjects);
let drive_hash = engine::compute_drive_hash(&vvs);
let peers: Vec<String> = vvs
.values()
.flat_map(|vv| vv.keys().cloned())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let peer_index: std::collections::HashMap<&str, usize> = peers
.iter()
.enumerate()
.map(|(i, p)| (p.as_str(), i))
.collect();
let mut resources: std::collections::HashMap<String, Vec<i32>> =
std::collections::HashMap::new();
for (subject, vv) in &vvs {
let mut counters = vec![0i32; peers.len()];
for (peer_id, &counter) in vv {
if let Some(&idx) = peer_index.get(peer_id.as_str()) {
counters[idx] = counter;
}
}
resources.insert(subject.clone(), counters);
}
protocol::encode_sync(drive, &drive_hash, &peers, &resources)
}