use pamoja_core::{Error, Result, Transport};
#[derive(Clone, Debug)]
pub struct DegradedLink<T> {
inner: T,
drop_every: u32,
window: Option<(u32, u32)>,
sends: u32,
}
impl<T> DegradedLink<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
drop_every: 0,
window: None,
sends: 0,
}
}
pub fn drop_every(mut self, n: u32) -> Self {
self.drop_every = n;
self
}
pub fn intermittent(mut self, up: u32, down: u32) -> Self {
self.window = if up + down == 0 {
None
} else {
Some((up, down))
};
self
}
pub fn into_inner(self) -> T {
self.inner
}
fn link_is_down(&self) -> bool {
match self.window {
Some((up, down)) => (self.sends - 1) % (up + down) >= up,
None => false,
}
}
fn packet_lost(&self) -> bool {
self.drop_every != 0 && self.sends.is_multiple_of(self.drop_every)
}
}
impl<T: Transport + Send> Transport for DegradedLink<T> {
async fn connect(&mut self) -> Result<()> {
self.inner.connect().await
}
async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
self.sends += 1;
if self.link_is_down() {
return Err(Error::Transport("link unreachable".to_owned()));
}
if self.packet_lost() {
return Err(Error::Transport("packet lost on a lossy link".to_owned()));
}
self.inner.send(topic, payload).await
}
async fn subscribe(&mut self, topic: &str) -> Result<()> {
self.inner.subscribe(topic).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct CountingTransport {
sent: Vec<Vec<u8>>,
}
impl Transport for CountingTransport {
async fn connect(&mut self) -> Result<()> {
Ok(())
}
async fn send(&mut self, _topic: &str, payload: &[u8]) -> Result<()> {
self.sent.push(payload.to_vec());
Ok(())
}
async fn subscribe(&mut self, _topic: &str) -> Result<()> {
Ok(())
}
}
async fn send_seq(link: &mut DegradedLink<CountingTransport>, count: u8) -> usize {
let mut errors = 0;
for i in 1..=count {
if link.send("t", &[i]).await.is_err() {
errors += 1;
}
}
errors
}
#[tokio::test]
async fn a_perfect_link_passes_every_send() {
let mut link = DegradedLink::new(CountingTransport::default());
assert_eq!(send_seq(&mut link, 3).await, 0);
assert_eq!(link.into_inner().sent.len(), 3);
}
#[tokio::test]
async fn loss_drops_every_nth_send() {
let inner = CountingTransport::default();
let mut link = DegradedLink::new(inner).drop_every(3);
assert_eq!(send_seq(&mut link, 6).await, 2); assert_eq!(
link.into_inner().sent,
vec![vec![1], vec![2], vec![4], vec![5]]
);
}
#[tokio::test]
async fn intermittency_cycles_between_up_and_down() {
let inner = CountingTransport::default();
let mut link = DegradedLink::new(inner).intermittent(2, 1);
assert_eq!(send_seq(&mut link, 6).await, 2); assert_eq!(
link.into_inner().sent,
vec![vec![1], vec![2], vec![4], vec![5]]
);
}
#[tokio::test]
async fn a_retry_after_a_drop_eventually_gets_through() {
let inner = CountingTransport::default();
let mut link = DegradedLink::new(inner).intermittent(1, 1);
assert!(link.send("t", b"x").await.is_ok()); assert!(link.send("t", b"x").await.is_err()); assert!(link.send("t", b"x").await.is_ok()); assert_eq!(link.into_inner().sent.len(), 2);
}
}