use std::sync::Arc;
use axum::extract::State;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::response::Response;
use futures_util::SinkExt as _;
use kernel::{AxCode, AxError, Seq};
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::TryRecvError;
use super::committed::Committed;
use crate::frames::{Answered, Ask, AskOutcome, Lagged, Sample, ServerFrame};
use crate::reception::inbound::Inbound;
use crate::reception::{SessionState, SessionStep, Stream, WelcomeFacts, decide_frame};
use crate::reply::{Delivered, Reply};
use super::config::{Answering, ShellState};
pub(crate) async fn upgrade(
State(state): State<Arc<ShellState>>,
ws: WebSocketUpgrade,
) -> Response {
ws.on_upgrade(move |socket| session(socket, state))
}
async fn session(mut socket: WebSocket, state: Arc<ShellState>) {
let mut phase = SessionState::AwaitingHello;
let mut inbound = Inbound::new();
let mut events = state.events.subscribe();
let mut deltas = state.deltas.subscribe();
let mut logs = state.logs.subscribe();
let mut outputs = state.outputs.subscribe();
let (refused, mut refusals) = tokio::sync::mpsc::unbounded_channel::<AxError>();
let mut stream = Stream::opening();
let mut watching: Option<Watching> = None;
loop {
tokio::select! {
incoming = socket.recv() => {
let Some(Ok(message)) = incoming else { return };
let Message::Text(text) = message else { continue };
let step = match inbound.read(&text) {
Ok(frame) => {
decide_frame(phase, frame, &state.face, WelcomeFacts { city: state.city.as_ref(), head: state.head.read(), epoch: state.epoch })
}
Err(unreadable) => unreadable,
};
match step {
SessionStep::Welcome(welcome) => {
phase = SessionState::Live;
if send(&mut socket, &ServerFrame::Welcome(*welcome)).await.is_err() {
return;
}
for written in (state.outputs_so_far)() {
if send(&mut socket, &ServerFrame::Output(written)).await.is_err() {
return;
}
}
}
SessionStep::Deliver(command) => {
let back = refused.clone();
let reply = Reply::to(move |error| match back.send(error) {
Ok(()) => Delivered::ToThePeer,
Err(_) => Delivered::PeerGone,
});
if let Err(error) = (state.commands)(*command, reply)
&& send(&mut socket, &ServerFrame::Refusal(Box::new(error))).await.is_err() {
return;
}
}
SessionStep::Answer(ask) => {
let frame = answered(Arc::clone(&state.queries), *ask).await;
if send(&mut socket, &frame).await.is_err() {
return;
}
}
SessionStep::Watch(watched) => {
let samples = watching
.take()
.map_or_else(|| state.monitor.samples.subscribe(), |(_, samples)| samples);
watching = Some(((state.monitor.watch)(watched), samples));
}
SessionStep::Release => watching = None,
SessionStep::Beat(beat) => (state.monitor.beat)(beat),
SessionStep::Refuse { error, close } => {
if send(&mut socket, &ServerFrame::Refusal(error)).await.is_err() {
return;
}
if close {
return;
}
}
}
}
late = refusals.recv() => {
let Some(error) = late else { return };
if send(&mut socket, &ServerFrame::Refusal(Box::new(error))).await.is_err() {
return;
}
}
event = events.recv() => {
let first = match event {
Ok(committed) => Arrival::Record(committed.record().seq(), committed),
Err(broadcast::error::RecvError::Lagged(_)) => Arrival::Skipped,
Err(broadcast::error::RecvError::Closed) => return,
};
let (arrivals, ending) = drained(first, &mut events);
let (said, next) = framed(stream, phase, arrivals);
stream = next;
if write_frame(&mut socket, said).await.is_err() {
return;
}
match ending {
Ending::Open => {}
Ending::Closed => return,
}
}
said = deltas.recv() => {
match said {
Ok(delta) => {
if phase == SessionState::Live
&& send(&mut socket, &ServerFrame::Delta(delta)).await.is_err() {
return;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => return,
}
}
written = logs.recv() => {
match written {
Ok(line) => {
if phase == SessionState::Live
&& send(&mut socket, &ServerFrame::Log(line)).await.is_err() {
return;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => return,
}
}
written = outputs.recv() => {
match written {
Ok(piece) => {
if phase == SessionState::Live
&& send(&mut socket, &ServerFrame::Output(piece)).await.is_err() {
return;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => return,
}
}
read = next_sample(&mut watching) => {
match read {
Ok(sample) => {
if send(&mut socket, &ServerFrame::Monitor(sample)).await.is_err() {
return;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => watching = None,
}
}
}
}
}
pub(crate) enum Arrival<R> {
Record(Seq, R),
Skipped,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Said<R> {
Lagged(Lagged),
Record(R),
}
pub(crate) const FRAME_RECORDS_MAX: usize = 256;
enum Ending {
Open,
Closed,
}
fn drained(
first: Arrival<Committed>,
events: &mut broadcast::Receiver<Committed>,
) -> (Vec<Arrival<Committed>>, Ending) {
let mut arrivals = vec![first];
while arrivals.len() < FRAME_RECORDS_MAX {
match events.try_recv() {
Ok(committed) => arrivals.push(Arrival::Record(committed.record().seq(), committed)),
Err(TryRecvError::Lagged(_)) => arrivals.push(Arrival::Skipped),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Closed) => return (arrivals, Ending::Closed),
}
}
(arrivals, Ending::Open)
}
pub(crate) fn framed<R>(
stream: Stream,
phase: SessionState,
arrivals: Vec<Arrival<R>>,
) -> (Vec<Said<R>>, Stream) {
let live = phase == SessionState::Live;
let mut said = Vec::with_capacity(arrivals.len());
let after = arrivals
.into_iter()
.fold(stream, |stream, arrival| match arrival {
Arrival::Record(seq, record) if live => {
let (lag, next) = stream.before(seq);
said.extend(lag.map(Said::Lagged));
said.push(Said::Record(record));
next
}
Arrival::Record(..) => stream,
Arrival::Skipped => stream.skipped(live),
});
(said, after)
}
async fn write_frame(socket: &mut WebSocket, said: Vec<Said<Committed>>) -> Result<(), ()> {
for each in said {
let text = match each {
Said::Lagged(lag) => serde_json::to_string(&ServerFrame::Lagged(lag))
.map_err(|_| ())?
.into(),
Said::Record(committed) => committed.frame(),
};
socket.feed(Message::Text(text)).await.map_err(|_| ())?;
}
socket.flush().await.map_err(|_| ())
}
async fn answered(answering: Answering, Ask { ask_id, query }: Ask) -> ServerFrame {
let (as_of, outcome) = match tokio::task::spawn_blocking(move || answering(query)).await {
Ok((as_of, Ok(answer))) => (as_of, AskOutcome::Answer(answer)),
Ok((as_of, Err(error))) => (as_of, AskOutcome::Refusal(error)),
Err(_) => (
Seq::FIRST,
AskOutcome::Refusal(
AxError::failure(
AxCode::StorageFatal,
"answer a query",
"the answering task did not finish",
)
.with_recovery("ask again; if it repeats, restart the server"),
),
),
};
ServerFrame::Answered(Box::new(Answered {
ask_id,
as_of,
outcome,
}))
}
type Watching = (Box<dyn Send>, broadcast::Receiver<Sample>);
async fn next_sample(
watching: &mut Option<Watching>,
) -> Result<Sample, broadcast::error::RecvError> {
match watching {
Some((_, samples)) => samples.recv().await,
None => std::future::pending().await,
}
}
async fn send(socket: &mut WebSocket, frame: &ServerFrame) -> Result<(), ()> {
let Ok(text) = serde_json::to_string(frame) else {
return Err(());
};
socket
.send(Message::Text(text.into()))
.await
.map_err(|_| ())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
reason = "test code"
)]
mod tests {
use kernel::Seq;
use proptest::prelude::*;
use super::{Arrival, Said, framed};
use crate::reception::{SessionState, Stream};
fn arrivals(ledger: &[u64], skipped: &[bool]) -> Vec<Arrival<u64>> {
let mut arrivals = Vec::new();
let mut lost = false;
for (seq, gone) in ledger.iter().zip(skipped) {
match (gone, lost) {
(true, true) => {}
(true, false) => arrivals.push(Arrival::Skipped),
(false, _) => arrivals.push(Arrival::Record(Seq::new(*seq), *seq)),
}
lost = *gone;
}
arrivals
}
fn told_in_frames(
arrivals: Vec<Arrival<u64>>,
cuts: &[usize],
phase: SessionState,
) -> (Vec<Said<u64>>, Stream) {
let mut points: Vec<usize> = cuts.iter().map(|cut| (*cut).min(arrivals.len())).collect();
points.sort_unstable();
let mut said = Vec::new();
let mut stream = Stream::opening();
let mut rest = arrivals;
let mut taken = 0;
for point in points {
let tail = rest.split_off(point - taken);
let (told, next) = framed(stream, phase, rest);
said.extend(told);
stream = next;
rest = tail;
taken = point;
}
let (told, next) = framed(stream, phase, rest);
said.extend(told);
(said, next)
}
proptest! {
#[test]
fn framing_tells_every_record_once_in_order(
start in 0u64..3,
gaps in proptest::collection::vec(1u64..4, 0..40),
skipped in proptest::collection::vec(any::<bool>(), 40),
cuts in proptest::collection::vec(0usize..48, 0..6),
) {
let ledger: Vec<u64> = gaps
.iter()
.scan(start, |seq, gap| {
*seq += gap;
Some(*seq)
})
.collect();
let handed = arrivals(&ledger, &skipped);
let kept: Vec<u64> = ledger
.iter()
.zip(&skipped)
.filter(|(_, gone)| !**gone)
.map(|(seq, _)| *seq)
.collect();
let whole = framed(Stream::opening(), SessionState::Live, arrivals(&ledger, &skipped));
let cut = told_in_frames(handed, &cuts, SessionState::Live);
prop_assert_eq!(&cut, &whole);
let records: Vec<u64> = whole
.0
.iter()
.filter_map(|said| match said {
Said::Record(seq) => Some(*seq),
Said::Lagged(_) => None,
})
.collect();
prop_assert_eq!(&records, &kept);
prop_assert!(records.windows(2).all(|pair| pair[0] < pair[1]));
if let (Some(first), Some(last)) = (records.first(), records.last()) {
for k in *first..=*last {
let told = whole.0.iter().any(|said| match said {
Said::Record(seq) => *seq == k,
Said::Lagged(lag) => lag.from.value() <= k && k <= lag.to.value(),
});
prop_assert!(told, "seq {} between {} and {} was told nothing", k, first, last);
}
}
let unwelcomed = framed(
Stream::opening(),
SessionState::AwaitingHello,
arrivals(&ledger, &skipped),
);
prop_assert!(unwelcomed.0.is_empty());
}
}
}