use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use tokio::sync::{mpsc, Notify};
use super::driver::{DriverEvent, Transport, WireMsg};
use super::replica::Message;
use super::types::NodeId;
pub type WireEnvelope = (u64, WireMsg);
const CONTROL_QUEUE_DEPTH: usize = 64;
const SEND_TIMEOUT: Duration = Duration::from_secs(2);
struct PeerQueues {
latest: Mutex<Option<WireMsg>>,
wake: Notify,
control: mpsc::Sender<WireMsg>,
}
pub struct HttpTransport {
me: NodeId,
peers: BTreeMap<NodeId, Arc<PeerQueues>>,
}
impl HttpTransport {
pub fn new(me: NodeId, peer_urls: BTreeMap<NodeId, String>, secret: Option<String>) -> Self {
let client = reqwest::Client::builder()
.timeout(SEND_TIMEOUT)
.build()
.expect("reqwest client");
let mut peers = BTreeMap::new();
for (peer, base) in peer_urls {
let (control_tx, control_rx) = mpsc::channel(CONTROL_QUEUE_DEPTH);
let q = Arc::new(PeerQueues {
latest: Mutex::new(None),
wake: Notify::new(),
control: control_tx,
});
tokio::spawn(run_peer_sender(
me,
peer,
base,
client.clone(),
secret.clone(),
q.clone(),
control_rx,
));
peers.insert(peer, q);
}
Self { me, peers }
}
}
impl Transport for HttpTransport {
fn send(&self, to: NodeId, msg: WireMsg) {
let Some(q) = self.peers.get(&to) else {
tracing::warn!(?to, "YRP send to unknown peer dropped");
return;
};
let cumulative = matches!(
msg,
WireMsg::Replica(Message::AppendEntries { .. })
| WireMsg::Replica(Message::InstallSnapshot { .. })
);
if cumulative {
*q.latest.lock() = Some(msg);
q.wake.notify_one();
} else if q.control.try_send(msg).is_err() {
tracing::debug!(?to, "YRP control queue full; message dropped");
}
}
}
async fn run_peer_sender(
me: NodeId,
peer: NodeId,
base: String,
client: reqwest::Client,
secret: Option<String>,
q: Arc<PeerQueues>,
mut control_rx: mpsc::Receiver<WireMsg>,
) {
let url = format!("{}/v1/yrp/msg", base.trim_end_matches('/'));
loop {
let msg = tokio::select! {
biased;
ctrl = control_rx.recv() => match ctrl {
Some(m) => m,
None => return, },
_ = q.wake.notified() => match q.latest.lock().take() {
Some(m) => m,
None => continue, },
};
let envelope: WireEnvelope = (me.0, msg);
let body = match bincode::serialize(&envelope) {
Ok(b) => b,
Err(e) => {
tracing::error!(error = %e, "YRP envelope serialize failed; dropped");
continue;
}
};
let mut req = client.post(&url).body(body);
if let Some(s) = &secret {
req = req.bearer_auth(s);
}
if let Err(e) = req.send().await {
tracing::debug!(?peer, error = %e, "YRP send failed (will retransmit)");
}
}
}
pub fn decode_envelope(body: &[u8]) -> Result<WireEnvelope, String> {
bincode::deserialize(body).map_err(|e| format!("malformed YRP envelope: {e}"))
}
pub fn deliver(
owner: &mpsc::UnboundedSender<DriverEvent>,
from: u64,
msg: WireMsg,
) -> Result<(), String> {
owner
.send(DriverEvent::Inbound {
from: NodeId(from),
msg,
})
.map_err(|_| "YRP driver not running".to_string())
}
#[cfg(test)]
mod tests {
use super::super::replica::Payload;
use super::super::types::{LogPosition, Term};
use super::*;
#[test]
fn wire_envelope_bincode_round_trip() {
let entry = super::super::replica::LogEntry {
term: Term(3),
payload: Payload::Op(vec![1, 2, 3, 255]),
key: Some(42),
activate: None,
};
let msg = WireMsg::Replica(Message::AppendEntries {
term: Term(3),
leader: NodeId(1),
prev: LogPosition { term: 2, index: 9 },
entries: vec![entry],
commit: 9,
});
let env: WireEnvelope = (1, msg);
let bytes = bincode::serialize(&env).unwrap();
let (from, back): WireEnvelope = bincode::deserialize(&bytes).unwrap();
assert_eq!(from, 1);
match back {
WireMsg::Replica(Message::AppendEntries { entries, .. }) => {
assert_eq!(entries[0].payload, Payload::Op(vec![1, 2, 3, 255]));
assert_eq!(entries[0].key, Some(42));
}
other => panic!("wrong decode: {other:?}"),
}
}
#[tokio::test]
async fn replication_messages_coalesce_to_latest() {
let (control_tx, _control_rx) = mpsc::channel(CONTROL_QUEUE_DEPTH);
let q = Arc::new(PeerQueues {
latest: Mutex::new(None),
wake: Notify::new(),
control: control_tx,
});
let transport = HttpTransport {
me: NodeId(1),
peers: [(NodeId(2), q.clone())].into_iter().collect(),
};
let hb = |commit| {
WireMsg::Replica(Message::AppendEntries {
term: Term(1),
leader: NodeId(1),
prev: LogPosition::ZERO,
entries: vec![],
commit,
})
};
transport.send(NodeId(2), hb(1));
transport.send(NodeId(2), hb(2));
let latest = q.latest.lock().take().expect("slot filled");
match latest {
WireMsg::Replica(Message::AppendEntries { commit, .. }) => assert_eq!(commit, 2),
other => panic!("wrong slot content: {other:?}"),
}
}
}