use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use russh::{ChannelMsg, ChannelReadHalf};
use tokio::io::{AsyncRead, ReadBuf};
type Pending = Pin<Box<dyn Future<Output = (ChannelReadHalf, Option<ChannelMsg>)> + Send>>;
enum State {
Idle(ChannelReadHalf),
Waiting(Pending),
Ended,
}
pub struct ChannelReader {
state: State,
partial: Option<(ChannelMsg, usize)>,
}
impl ChannelReader {
pub fn new(half: ChannelReadHalf) -> Self {
ChannelReader {
state: State::Idle(half),
partial: None,
}
}
}
impl std::fmt::Debug for ChannelReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChannelReader").finish_non_exhaustive()
}
}
fn payload(message: &ChannelMsg) -> &[u8] {
match message {
ChannelMsg::Data { data } => data,
_ => &[],
}
}
fn next_data(mut half: ChannelReadHalf) -> Pending {
Box::pin(async move {
loop {
match half.wait().await {
Some(ChannelMsg::Data { data }) => {
return (half, Some(ChannelMsg::Data { data }));
}
Some(ChannelMsg::Eof) | Some(ChannelMsg::Close) | None => return (half, None),
Some(_) => continue,
}
}
})
}
impl AsyncRead for ChannelReader {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let me = self.get_mut();
loop {
if let Some((message, taken)) = me.partial.as_mut() {
let data = payload(message);
let readable = buf.remaining().min(data.len().saturating_sub(*taken));
buf.put_slice(&data[*taken..*taken + readable]);
*taken += readable;
if *taken >= data.len() {
me.partial = None;
}
return Poll::Ready(Ok(()));
}
match std::mem::replace(&mut me.state, State::Ended) {
State::Ended => return Poll::Ready(Ok(())),
State::Idle(half) => me.state = State::Waiting(next_data(half)),
State::Waiting(mut waiting) => match waiting.as_mut().poll(cx) {
Poll::Pending => {
me.state = State::Waiting(waiting);
return Poll::Pending;
}
Poll::Ready((half, Some(message))) => {
me.state = State::Idle(half);
me.partial = Some((message, 0));
}
Poll::Ready((_, None)) => {
me.state = State::Ended;
return Poll::Ready(Ok(()));
}
},
}
}
}
}
pub type SessionOver = tokio::sync::watch::Receiver<bool>;
pub async fn session_over(mut over: SessionOver) {
let _ = over.wait_for(|ended| *ended).await;
}
pub fn session_ended_error() -> io::Error {
io::Error::new(
io::ErrorKind::ConnectionAborted,
"the client's ssh session ended mid-transfer (it hung up, or its link went silent \
and its keepalives went unanswered)",
)
}
pub const SESSION_END_GRACE: std::time::Duration = std::time::Duration::from_secs(2);
pub const CROWDED_SESSION_END_GRACE: std::time::Duration = std::time::Duration::from_millis(200);
pub fn grace_for(
calm: std::time::Duration,
crowded: std::time::Duration,
queued: usize,
) -> std::time::Duration {
if queued > 0 {
crowded.min(calm)
} else {
calm
}
}
#[derive(Clone, Default)]
pub struct Cancel(std::sync::Arc<CancelInner>);
#[derive(Default)]
struct CancelInner {
set: std::sync::atomic::AtomicBool,
wake: tokio::sync::Notify,
}
impl Cancel {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.0.set.store(true, std::sync::atomic::Ordering::SeqCst);
self.0.wake.notify_waiters();
}
pub fn is_cancelled(&self) -> bool {
self.0.set.load(std::sync::atomic::Ordering::SeqCst)
}
pub async fn cancelled(&self) {
loop {
let notified = self.0.wake.notified();
if self.is_cancelled() {
return;
}
notified.await;
}
}
}
impl std::fmt::Debug for Cancel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Cancel").field(&self.is_cancelled()).finish()
}
}
pub async fn serve_until_session_ends<T, E>(
serve: impl Future<Output = Result<T, E>>,
over: SessionOver,
cancel: &Cancel,
grace: impl FnOnce() -> std::time::Duration,
ended: impl FnOnce() -> E,
) -> Result<T, E> {
let mut serve = std::pin::pin!(serve);
tokio::select! {
served = &mut serve => served,
() = session_over(over) => match tokio::time::timeout(grace(), &mut serve).await {
Ok(served) => served,
Err(_) => {
cancel.cancel();
tracing::debug!(target: "gunnar::ssh", event = "ssh.transfer.told_to_stop", "the session ended and the transfer did not end within the grace; it is told to stop and its worker waits for it");
match serve.await {
Ok(served) => Ok(served),
Err(_) => Err(ended()),
}
}
},
}
}
pub struct SessionBound<W> {
inner: W,
over: SessionOver,
ending: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
}
impl<W> SessionBound<W> {
pub fn new(inner: W, over: SessionOver) -> Self {
SessionBound {
inner,
over,
ending: None,
}
}
fn poll_over(&mut self, cx: &mut Context<'_>) -> Poll<()> {
if *self.over.borrow() {
return Poll::Ready(());
}
let over = self.over.clone();
let ending = self
.ending
.get_or_insert_with(|| Box::pin(session_over(over)));
ending.as_mut().poll(cx)
}
}
impl<W> std::fmt::Debug for SessionBound<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionBound").finish_non_exhaustive()
}
}
impl<W: tokio::io::AsyncWrite + Unpin> tokio::io::AsyncWrite for SessionBound<W> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let me = self.get_mut();
match Pin::new(&mut me.inner).poll_write(cx, buf) {
Poll::Ready(r) => Poll::Ready(r),
Poll::Pending => match me.poll_over(cx) {
Poll::Ready(()) => Poll::Ready(Err(session_ended_error())),
Poll::Pending => Poll::Pending,
},
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let me = self.get_mut();
match Pin::new(&mut me.inner).poll_flush(cx) {
Poll::Ready(r) => Poll::Ready(r),
Poll::Pending => match me.poll_over(cx) {
Poll::Ready(()) => Poll::Ready(Err(session_ended_error())),
Poll::Pending => Poll::Pending,
},
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let me = self.get_mut();
match Pin::new(&mut me.inner).poll_shutdown(cx) {
Poll::Ready(r) => Poll::Ready(r),
Poll::Pending => match me.poll_over(cx) {
Poll::Ready(()) => Poll::Ready(Err(session_ended_error())),
Poll::Pending => Poll::Pending,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn ended_session() -> (tokio::sync::watch::Sender<bool>, SessionOver) {
let (tx, rx) = tokio::sync::watch::channel(false);
tx.send_replace(true);
(tx, rx)
}
#[tokio::test]
async fn a_client_that_hung_up_after_being_served_is_no_failed_transfer() {
let (_tx, over) = ended_session();
let serve = async {
tokio::time::sleep(Duration::from_millis(50)).await;
Ok::<(), io::Error>(())
};
let served = serve_until_session_ends(
serve,
over,
&Cancel::new(),
|| SESSION_END_GRACE,
session_ended_error,
)
.await;
assert!(
served.is_ok(),
"a transfer that ended cleanly was called failed: {served:?}"
);
}
#[test]
fn the_grace_is_shorter_when_others_wait_for_the_fan() {
let (calm, crowded) = (Duration::from_secs(2), Duration::from_millis(200));
assert_eq!(
grace_for(calm, crowded, 0),
calm,
"nobody waits: the whole grace"
);
assert_eq!(
grace_for(calm, crowded, 1),
crowded,
"one waits: the crowded grace"
);
assert_eq!(
grace_for(Duration::from_millis(100), crowded, 5),
Duration::from_millis(100),
"never longer than the calm grace"
);
}
#[tokio::test]
async fn a_transfer_is_cut_after_the_grace_it_is_given() {
let (_tx, over) = ended_session();
let cancel = Cancel::new();
let heard = cancel.clone();
let serve = async move {
heard.cancelled().await;
Err::<(), io::Error>(io::Error::new(
io::ErrorKind::ConnectionAborted,
"told to stop",
))
};
let started = std::time::Instant::now();
let served = serve_until_session_ends(
serve,
over,
&cancel,
|| Duration::from_millis(50),
session_ended_error,
)
.await;
assert!(served.is_err());
assert!(
started.elapsed() < Duration::from_secs(1),
"cut after {:?}, not the 50 ms it was given",
started.elapsed()
);
}
#[tokio::test]
async fn a_transfer_still_working_when_the_session_ends_is_cut() {
let (_tx, over) = ended_session();
let cancel = Cancel::new();
let heard = cancel.clone();
let serve = async move {
heard.cancelled().await;
Err::<(), io::Error>(io::Error::new(
io::ErrorKind::ConnectionAborted,
"told to stop",
))
};
let served = tokio::time::timeout(
Duration::from_secs(10),
serve_until_session_ends(
serve,
over,
&cancel,
|| SESSION_END_GRACE,
session_ended_error,
),
)
.await
.expect("a transfer outlived its session by 10 s");
let err =
served.expect_err("a transfer still working when its session ended was called served");
assert_eq!(err.kind(), io::ErrorKind::ConnectionAborted, "{err}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_hung_up_client_cannot_make_blocking_work_exceed_the_fan() {
use crate::governor::OpClass;
use crate::pipeline::{Job, Pipeline};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
const WORK: Duration = Duration::from_secs(3);
let fan: Pipeline<Job> = Pipeline::start(OpClass::Clone, 1, 4, |job: Job| job());
let live = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let (done_tx, mut done_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
for _ in 0..2 {
let (live, peak, done) = (Arc::clone(&live), Arc::clone(&peak), done_tx.clone());
let handle = tokio::runtime::Handle::current();
let (session, over) = ended_session();
fan.submit(Box::new(move || {
handle.block_on(async move {
let _session = session;
let serve = async move {
tokio::task::spawn_blocking(move || {
let now = live.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
let until = std::time::Instant::now() + WORK;
while std::time::Instant::now() < until {
std::thread::sleep(Duration::from_millis(10));
}
live.fetch_sub(1, Ordering::SeqCst);
Ok::<(), io::Error>(())
})
.await
.map_err(io::Error::other)?
};
let _ = serve_until_session_ends(
serve,
over,
&Cancel::new(),
|| SESSION_END_GRACE,
session_ended_error,
)
.await;
});
let _ = done.send(());
}))
.expect("the fan takes the transfer");
}
for _ in 0..2 {
done_rx.recv().await.expect("a transfer finished");
}
while live.load(Ordering::SeqCst) > 0 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
let peak = peak.load(Ordering::SeqCst);
assert!(
peak <= 1,
"a fan of one worker had {peak} transfers' blocking work running at once: the worker \
was released while the work of a client that hung up went on"
);
}
#[tokio::test]
async fn a_transfer_with_its_session_alive_is_its_own_verdict() {
let (_tx, over) = tokio::sync::watch::channel(false);
let ok = serve_until_session_ends(
async { Ok::<u8, io::Error>(7) },
over.clone(),
&Cancel::new(),
|| SESSION_END_GRACE,
session_ended_error,
)
.await;
assert_eq!(ok.unwrap(), 7);
let err = serve_until_session_ends(
async { Err::<u8, io::Error>(io::Error::other("store")) },
over,
&Cancel::new(),
|| SESSION_END_GRACE,
session_ended_error,
)
.await;
assert_eq!(err.unwrap_err().to_string(), "store");
}
}