use std::io;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::scenario::{self, Scenario};
use crate::store::{lock, Outcome, ReqLog, Shared};
#[derive(Debug)]
pub struct MockNexus {
shared: Arc<Shared>,
addr: SocketAddr,
stop: Arc<AtomicBool>,
}
impl MockNexus {
pub fn start(scenario: Scenario) -> std::io::Result<Self> {
Self::start_on(scenario, SocketAddr::from(([127, 0, 0, 1], 0)))
}
pub fn start_on(scenario: Scenario, addr: SocketAddr) -> std::io::Result<Self> {
let listener = TcpListener::bind(addr)?;
let bound = listener.local_addr()?;
let shared = Arc::new(Shared::hosted(scenario, format!("http://{bound}/")));
let stop = Arc::new(AtomicBool::new(false));
spawn_acceptor(listener, Arc::clone(&shared), Arc::clone(&stop));
Ok(Self {
shared,
addr: bound,
stop,
})
}
pub fn start_group(members: &[&MockNexus]) -> std::io::Result<Self> {
if members.len() < 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a group repository needs at least two members",
));
}
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
let bound = listener.local_addr()?;
let shared = Arc::new(Shared::group(members));
let stop = Arc::new(AtomicBool::new(false));
spawn_acceptor(listener, Arc::clone(&shared), Arc::clone(&stop));
Ok(Self {
shared,
addr: bound,
stop,
})
}
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn base_url(&self) -> String {
format!("http://{}/", self.addr)
}
pub fn store_get(&self, path: &str) -> Option<Vec<u8>> {
lock(&self.shared.store).get(path).cloned()
}
pub fn insert(&self, path: &str, bytes: &[u8]) {
lock(&self.shared.store).insert(path.to_owned(), bytes.to_vec());
}
pub fn requests(&self) -> Vec<ReqLog> {
lock(&self.shared.log).clone()
}
pub fn put_count(&self, path: &str) -> usize {
self.requests()
.iter()
.filter(|req| {
req.method == "PUT"
&& req.path == path
&& matches!(req.outcome, Outcome::Status(200..=299))
})
.count()
}
pub fn enable_drift(&self) {
self.shared.drift.store(true, Ordering::Relaxed);
}
pub fn disable_drift(&self) {
self.shared.drift.store(false, Ordering::Relaxed);
}
}
fn is_transient_accept(e: &io::Error) -> bool {
matches!(
e.kind(),
io::ErrorKind::ConnectionAborted
| io::ErrorKind::ConnectionReset
| io::ErrorKind::Interrupted
)
}
fn spawn_acceptor(listener: TcpListener, shared: Arc<Shared>, stop: Arc<AtomicBool>) {
std::thread::spawn(move || loop {
match listener.accept() {
Ok((conn, _)) => {
if stop.load(Ordering::Relaxed) {
break;
}
let conn_shared = Arc::clone(&shared);
std::thread::spawn(move || scenario::serve(&conn_shared, conn));
}
Err(e) if is_transient_accept(&e) => {
eprintln!("mock: accept rode out a transient error: {e}");
}
Err(e) => {
eprintln!("mock: acceptor stopping on a fatal accept error: {e}");
break;
}
}
});
}
impl Drop for MockNexus {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
let _ = TcpStream::connect(self.addr);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transient_accept_classification() {
let transient = |kind| is_transient_accept(&io::Error::new(kind, "probe"));
assert!(transient(io::ErrorKind::ConnectionAborted));
assert!(transient(io::ErrorKind::ConnectionReset));
assert!(transient(io::ErrorKind::Interrupted));
assert!(!transient(io::ErrorKind::PermissionDenied));
assert!(!transient(io::ErrorKind::AddrInUse));
}
}