use std::collections::BTreeSet;
use ts_collective::{Group, Mesh, Reduce};
use ts_scheduler::{CacheEvent, CacheEventKind, CacheOpView};
use crate::codec::{Reader, Writer};
#[derive(Debug, thiserror::Error)]
pub enum CacheOpError {
#[error(
"{acknowledged} cache acknowledgments arrived with only {in_flight} cache ops in flight"
)]
Unaccounted {
acknowledged: usize,
in_flight: usize,
},
#[error("rank {0} sent malformed cache acknowledgments")]
Malformed(usize),
#[error(transparent)]
Collective(#[from] ts_collective::Error),
}
fn kind_name(kind: CacheEventKind) -> &'static str {
match kind {
CacheEventKind::WriteBackDone => "WriteBackDoneEvent",
CacheEventKind::LoadBackDone => "LoadBackDoneEvent",
CacheEventKind::SnapshotDone => "SnapshotDoneEvent",
CacheEventKind::RestoreDone => "RestoreDoneEvent",
_ => unreachable!("a cache event kind the bridge does not define"),
}
}
const KINDS: [CacheEventKind; 4] = [
CacheEventKind::WriteBackDone,
CacheEventKind::LoadBackDone,
CacheEventKind::SnapshotDone,
CacheEventKind::RestoreDone,
];
type Key = (u32, &'static str);
fn key(event: &CacheEvent) -> Key {
(event.op_id, kind_name(event.kind))
}
fn encode(events: &[CacheEvent]) -> Vec<u8> {
let mut out = Writer::new();
out.count(events.len());
for event in events {
let code = KINDS
.iter()
.position(|kind| *kind == event.kind)
.expect("a bridge kind");
out.u8(code as u8);
out.u32(event.op_id);
}
out.finish()
}
fn decode(bytes: &[u8]) -> Option<Vec<CacheEvent>> {
let mut reader = Reader::new(bytes);
let count = reader.count()?;
let events = (0..count)
.map(|_| {
Some(CacheEvent {
kind: *KINDS.get(usize::from(reader.u8()?))?,
op_id: reader.u32()?,
})
})
.collect::<Option<Vec<_>>>()?;
reader.finish(events)
}
#[derive(Debug, Default)]
pub struct CacheOps {
open: usize,
in_flight: usize,
pending: Vec<CacheEvent>,
}
impl CacheOps {
pub fn new() -> Self {
Self::default()
}
pub fn open(&self) -> usize {
self.open
}
pub fn dispatched(&mut self, ops: &[CacheOpView]) {
self.open += ops.len();
self.in_flight += ops.len();
}
pub fn acknowledged(
&mut self,
polled: Vec<CacheEvent>,
mesh: &mut Mesh,
replica: &Group,
) -> Result<Vec<CacheEvent>, CacheOpError> {
if polled.len() > self.in_flight {
return Err(CacheOpError::Unaccounted {
acknowledged: polled.len(),
in_flight: self.in_flight,
});
}
self.in_flight -= polled.len();
for event in polled {
match self
.pending
.iter_mut()
.find(|held| key(held) == key(&event))
{
Some(held) => *held = event,
None => self.pending.push(event),
}
}
if replica.size() == 1 {
let ready = std::mem::take(&mut self.pending);
self.open -= ready.len();
return Ok(ready);
}
let local_work = self.in_flight != 0 || !self.pending.is_empty();
let agreed = mesh.all_reduce(replica, &[i64::from(local_work)], Reduce::Max)?;
if agreed[0] == 0 {
return Ok(Vec::new());
}
let gathered = mesh.all_gather(replica, &encode(&self.pending))?;
let mut members = Vec::with_capacity(gathered.len());
for (index, bytes) in gathered.iter().enumerate() {
members.push(decode(bytes).ok_or(CacheOpError::Malformed(replica.ranks()[index]))?);
}
let common: BTreeSet<Key> = members
.iter()
.map(|events| events.iter().map(key).collect::<BTreeSet<Key>>())
.reduce(|common, keys| common.intersection(&keys).copied().collect())
.unwrap_or_default();
let mut ready: Vec<CacheEvent> = members
.swap_remove(0)
.into_iter()
.filter(|event| common.contains(&key(event)))
.collect();
ready.sort_by_key(key);
self.pending.retain(|event| !common.contains(&key(event)));
self.open -= ready.len();
Ok(ready)
}
}
#[cfg(test)]
mod tests {
use std::{thread, time::Duration};
use ts_collective::Listener;
use ts_scheduler::CacheOpKind;
use super::*;
fn op(kind: CacheOpKind, op_id: u32) -> CacheOpView {
CacheOpView { kind, op_id }
}
fn ack(kind: CacheEventKind, op_id: u32) -> CacheEvent {
CacheEvent { kind, op_id }
}
fn alone() -> (Mesh, Group) {
let listener = Listener::bind("127.0.0.1").unwrap();
let address = format!("127.0.0.1:{}", listener.port().unwrap());
let mesh = Mesh::connect(0, listener, &[address], Duration::from_secs(5)).unwrap();
(mesh, Group::new(0, vec![0], 0).unwrap())
}
#[test]
fn one_rank_advances_what_landed_in_landing_order() {
let (mut mesh, replica) = alone();
let mut ops = CacheOps::new();
ops.dispatched(&[
op(CacheOpKind::WriteBack, 1),
op(CacheOpKind::SnapshotStore, 2),
op(CacheOpKind::SnapshotStore, 3),
]);
let polled = vec![
ack(CacheEventKind::SnapshotDone, 3),
ack(CacheEventKind::WriteBackDone, 1),
];
let ready = ops
.acknowledged(polled.clone(), &mut mesh, &replica)
.unwrap();
assert_eq!(ready, polled);
assert_eq!(ops.open(), 1);
}
#[test]
fn an_acknowledgment_nothing_dispatched_is_an_error() {
let (mut mesh, replica) = alone();
let mut ops = CacheOps::new();
ops.dispatched(&[op(CacheOpKind::LoadBack, 7)]);
let polled = vec![
ack(CacheEventKind::LoadBackDone, 7),
ack(CacheEventKind::LoadBackDone, 8),
];
let error = ops.acknowledged(polled, &mut mesh, &replica).unwrap_err();
assert!(matches!(
error,
CacheOpError::Unaccounted {
acknowledged: 2,
in_flight: 1
}
));
}
#[test]
fn a_replica_advances_only_what_every_rank_holds() {
let listeners: Vec<_> = (0..2)
.map(|_| Listener::bind("127.0.0.1").unwrap())
.collect();
let addresses: Vec<String> = listeners
.iter()
.map(|listener| format!("127.0.0.1:{}", listener.port().unwrap()))
.collect();
let polls = [
vec![
vec![
ack(CacheEventKind::WriteBackDone, 2),
ack(CacheEventKind::SnapshotDone, 1),
],
vec![],
vec![ack(CacheEventKind::RestoreDone, 4)],
],
vec![
vec![ack(CacheEventKind::SnapshotDone, 1)],
vec![
ack(CacheEventKind::RestoreDone, 4),
ack(CacheEventKind::WriteBackDone, 2),
],
vec![],
],
];
let threads: Vec<_> = listeners
.into_iter()
.zip(polls)
.enumerate()
.map(|(rank, (listener, polls))| {
let addresses = addresses.clone();
thread::spawn(move || {
let mut mesh =
Mesh::connect(rank, listener, &addresses, Duration::from_secs(5)).unwrap();
let replica = Group::new(0, vec![0, 1], rank).unwrap();
let mut ops = CacheOps::new();
ops.dispatched(&[
op(CacheOpKind::SnapshotStore, 1),
op(CacheOpKind::WriteBack, 2),
op(CacheOpKind::SnapshotRestore, 4),
]);
let rounds: Vec<Vec<CacheEvent>> = polls
.into_iter()
.map(|polled| ops.acknowledged(polled, &mut mesh, &replica).unwrap())
.collect();
(rounds, ops.open())
})
})
.collect();
for thread in threads {
let (rounds, open) = thread.join().unwrap();
assert_eq!(
rounds,
[
vec![ack(CacheEventKind::SnapshotDone, 1)],
vec![ack(CacheEventKind::WriteBackDone, 2)],
vec![ack(CacheEventKind::RestoreDone, 4)],
]
);
assert_eq!(open, 0);
}
}
}