Implement IDLE #72

Merged
quentin merged 9 commits from feat/idle into main 2024-01-19 14:04:04 +00:00
8 changed files with 110 additions and 23 deletions
Showing only changes of commit e1161cab0e - Show all commits

View file

@ -238,6 +238,22 @@ impl<S: BayouState> Bayou<S> {
Ok(()) Ok(())
} }
pub async fn idle_sync(&mut self) -> Result<()> {
tracing::debug!("start idle_sync");
loop {
tracing::trace!("idle_sync loop");
let fut_notif = self.watch.learnt_remote_update.notified();
if self.last_sync_watch_ct != *self.watch.rx.borrow() {
break
}
fut_notif.await;
}
tracing::trace!("idle_sync done");
self.sync().await?;
Ok(())
}
/// Applies a new operation on the state. Once this function returns, /// Applies a new operation on the state. Once this function returns,
/// the operation has been safely persisted to storage backend. /// the operation has been safely persisted to storage backend.
/// Make sure to call `.opportunistic_sync()` before doing this, /// Make sure to call `.opportunistic_sync()` before doing this,
@ -257,7 +273,7 @@ impl<S: BayouState> Bayou<S> {
seal_serialize(&op, &self.key)?, seal_serialize(&op, &self.key)?,
); );
self.storage.row_insert(vec![row_val]).await?; self.storage.row_insert(vec![row_val]).await?;
self.watch.notify.notify_one(); self.watch.propagate_local_update.notify_one();
let new_state = self.state().apply(&op); let new_state = self.state().apply(&op);
self.history.push((ts, op, Some(new_state))); self.history.push((ts, op, Some(new_state)));
@ -423,7 +439,8 @@ impl<S: BayouState> Bayou<S> {
struct K2vWatch { struct K2vWatch {
target: storage::RowRef, target: storage::RowRef,
rx: watch::Receiver<storage::RowRef>, rx: watch::Receiver<storage::RowRef>,
notify: Notify, propagate_local_update: Notify,
learnt_remote_update: Notify,
} }
impl K2vWatch { impl K2vWatch {
@ -434,9 +451,10 @@ impl K2vWatch {
let storage = creds.storage.build().await?; let storage = creds.storage.build().await?;
let (tx, rx) = watch::channel::<storage::RowRef>(target.clone()); let (tx, rx) = watch::channel::<storage::RowRef>(target.clone());
let notify = Notify::new(); let propagate_local_update = Notify::new();
let learnt_remote_update = Notify::new();
let watch = Arc::new(K2vWatch { target, rx, notify }); let watch = Arc::new(K2vWatch { target, rx, propagate_local_update, learnt_remote_update });
tokio::spawn(Self::background_task(Arc::downgrade(&watch), storage, tx)); tokio::spawn(Self::background_task(Arc::downgrade(&watch), storage, tx));
@ -459,7 +477,12 @@ impl K2vWatch {
this.target.uid.shard, this.target.uid.sort this.target.uid.shard, this.target.uid.sort
); );
tokio::select!( tokio::select!(
// Needed to exit: will force a loop iteration every minutes,
// that will stop the loop if other Arc references have been dropped
// and free resources. Otherwise we would be blocked waiting forever...
_ = tokio::time::sleep(Duration::from_secs(60)) => continue, _ = tokio::time::sleep(Duration::from_secs(60)) => continue,
// Watch if another instance has modified the log
update = storage.row_poll(&row) => { update = storage.row_poll(&row) => {
match update { match update {
Err(e) => { Err(e) => {
@ -471,10 +494,13 @@ impl K2vWatch {
if tx.send(row.clone()).is_err() { if tx.send(row.clone()).is_err() {
break; break;
} }
this.learnt_remote_update.notify_waiters();
} }
} }
} }
_ = this.notify.notified() => {
// It appears we have modified the log, informing other people
_ = this.propagate_local_update.notified() => {
let rand = u128::to_be_bytes(thread_rng().gen()).to_vec(); let rand = u128::to_be_bytes(thread_rng().gen()).to_vec();
let row_val = storage::RowVal::new(row.clone(), rand); let row_val = storage::RowVal::new(row.clone(), rand);
if let Err(e) = storage.row_insert(vec![row_val]).await if let Err(e) = storage.row_insert(vec![row_val]).await

View file

@ -82,7 +82,7 @@ pub async fn dispatch<'a>(
// IDLE extension (rfc2177) // IDLE extension (rfc2177)
CommandBody::Idle => { CommandBody::Idle => {
Ok(( Ok((
Response::build().to_req(ctx.req).message("DUMMY response due to anti-pattern").ok()?, Response::build().to_req(ctx.req).message("DUMMY command due to anti-pattern in the code").ok()?,
flow::Transition::Idle(tokio::sync::Notify::new()), flow::Transition::Idle(tokio::sync::Notify::new()),
)) ))
} }

View file

@ -21,7 +21,7 @@ pub enum State {
NotAuthenticated, NotAuthenticated,
Authenticated(Arc<User>), Authenticated(Arc<User>),
Selected(Arc<User>, MailboxView, MailboxPerm), Selected(Arc<User>, MailboxView, MailboxPerm),
Idle(Arc<User>, MailboxView, MailboxPerm, Notify), Idle(Arc<User>, MailboxView, MailboxPerm, Arc<Notify>),
Logout, Logout,
} }
@ -56,7 +56,7 @@ impl State {
State::Authenticated(u.clone()) State::Authenticated(u.clone())
} }
(State::Selected(u, m, p), Transition::Idle(s)) => { (State::Selected(u, m, p), Transition::Idle(s)) => {
State::Idle(u, m, p, s) State::Idle(u, m, p, Arc::new(s))
}, },
(State::Idle(u, m, p, _), Transition::UnIdle) => { (State::Idle(u, m, p, _), Transition::UnIdle) => {
State::Selected(u, m, p) State::Selected(u, m, p)

View file

@ -97,11 +97,14 @@ impl Server {
} }
} }
use tokio::sync::mpsc::*; use tokio::sync::mpsc::*;
use tokio_util::bytes::BytesMut;
use tokio::sync::Notify;
use std::sync::Arc;
enum LoopMode { enum LoopMode {
Quit, Quit,
Interactive, Interactive,
Idle, Idle(BytesMut, Arc<Notify>),
} }
// @FIXME a full refactor of this part of the code will be needed sooner or later // @FIXME a full refactor of this part of the code will be needed sooner or later
@ -190,7 +193,7 @@ impl NetLoop {
loop { loop {
mode = match mode { mode = match mode {
LoopMode::Interactive => self.interactive_mode().await?, LoopMode::Interactive => self.interactive_mode().await?,
LoopMode::Idle => self.idle_mode().await?, LoopMode::Idle(buff, stop) => self.idle_mode(buff, stop).await?,
LoopMode::Quit => break, LoopMode::Quit => break,
} }
} }
@ -238,11 +241,11 @@ impl NetLoop {
} }
self.server.enqueue_status(response.completion); self.server.enqueue_status(response.completion);
}, },
Some(ResponseOrIdle::StartIdle) => { Some(ResponseOrIdle::StartIdle(stop)) => {
let cr = CommandContinuationRequest::basic(None, "Idling")?; let cr = CommandContinuationRequest::basic(None, "Idling")?;
self.server.enqueue_continuation(cr); self.server.enqueue_continuation(cr);
self.cmd_tx.try_send(Request::Idle)?; self.cmd_tx.try_send(Request::Idle)?;
return Ok(LoopMode::Idle) return Ok(LoopMode::Idle(BytesMut::new(), stop))
}, },
None => { None => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
@ -260,9 +263,19 @@ impl NetLoop {
Ok(LoopMode::Interactive) Ok(LoopMode::Interactive)
} }
async fn idle_mode(&mut self) -> Result<LoopMode> { async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> {
// Flush send
loop {
match self.server.progress_send().await? {
Some(..) => continue,
None => break,
}
}
tokio::select! { tokio::select! {
// Receiving IDLE event from background
maybe_msg = self.resp_rx.recv() => match maybe_msg { maybe_msg = self.resp_rx.recv() => match maybe_msg {
// Session decided idle is terminated
Some(ResponseOrIdle::Response(response)) => { Some(ResponseOrIdle::Response(response)) => {
for body_elem in response.body.into_iter() { for body_elem in response.body.into_iter() {
let _handle = match body_elem { let _handle = match body_elem {
@ -273,6 +286,7 @@ impl NetLoop {
self.server.enqueue_status(response.completion); self.server.enqueue_status(response.completion);
return Ok(LoopMode::Interactive) return Ok(LoopMode::Interactive)
}, },
// Session has some information for user
Some(ResponseOrIdle::IdleEvent(elems)) => { Some(ResponseOrIdle::IdleEvent(elems)) => {
for body_elem in elems.into_iter() { for body_elem in elems.into_iter() {
let _handle = match body_elem { let _handle = match body_elem {
@ -280,17 +294,43 @@ impl NetLoop {
Body::Status(s) => self.server.enqueue_status(s), Body::Status(s) => self.server.enqueue_status(s),
}; };
} }
return Ok(LoopMode::Idle) self.cmd_tx.try_send(Request::Idle)?;
return Ok(LoopMode::Idle(buff, stop))
}, },
// Session crashed
None => { None => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
return Ok(LoopMode::Interactive) return Ok(LoopMode::Interactive)
}, },
Some(ResponseOrIdle::StartIdle) => unreachable!(),
} // Session can't start idling while already idling, it's a logic error!
Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"),
},
// User is trying to interact with us
_read_client_bytes = self.server.stream.read(&mut buff) => {
use imap_codec::decode::Decoder;
let codec = imap_codec::IdleDoneCodec::new();
match codec.decode(&buff) {
Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => {
// Session will be informed that it must stop idle
// It will generate the "done" message and change the loop mode
stop.notify_one()
},
Err(_) => (),
_ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."),
};
return Ok(LoopMode::Idle(buff, stop))
},
// When receiving a CTRL+C
_ = self.ctx.must_exit.changed() => {
self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
return Ok(LoopMode::Interactive)
},
}; };
/*self.cmd_tx.try_send(Request::Idle).unwrap();
Ok(LoopMode::Idle)*/
} }
} }

View file

@ -1,5 +1,4 @@
use imap_codec::imap_types::command::Command; use imap_codec::imap_types::command::Command;
use tokio::sync::Notify;
#[derive(Debug)] #[derive(Debug)]
pub enum Request { pub enum Request {

View file

@ -1,4 +1,6 @@
use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use tokio::sync::Notify;
use imap_codec::imap_types::command::Command; use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::core::Tag; use imap_codec::imap_types::core::Tag;
use imap_codec::imap_types::response::{Code, Data, Status}; use imap_codec::imap_types::response::{Code, Data, Status};
@ -116,6 +118,6 @@ impl<'a> Response<'a> {
#[derive(Debug)] #[derive(Debug)]
pub enum ResponseOrIdle { pub enum ResponseOrIdle {
Response(Response<'static>), Response(Response<'static>),
StartIdle, StartIdle(Arc<Notify>),
IdleEvent(Vec<Body<'static>>), IdleEvent(Vec<Body<'static>>),
} }

View file

@ -38,6 +38,16 @@ impl Instance {
_ => unreachable!(), _ => unreachable!(),
}; };
tokio::select! {
_ = stop.notified() => {
return Response::build()
.tag(imap_codec::imap_types::core::Tag::try_from("FIXME").unwrap())
.message("IDLE completed")
.ok()
.unwrap()
}
}
unimplemented!(); unimplemented!();
} }
@ -108,8 +118,8 @@ impl Instance {
.unwrap()); .unwrap());
} }
match self.state { match &self.state {
flow::State::Idle(..) => ResponseOrIdle::StartIdle, flow::State::Idle(_, _, _, n) => ResponseOrIdle::StartIdle(n.clone()),
_ => ResponseOrIdle::Response(resp), _ => ResponseOrIdle::Response(resp),
} }
} }

View file

@ -67,6 +67,11 @@ impl Mailbox {
self.mbox.write().await.opportunistic_sync().await self.mbox.write().await.opportunistic_sync().await
} }
/// Block until a sync has been done (due to changes in the event log)
pub async fn idle_sync(&self) -> Result<()> {
self.mbox.write().await.idle_sync().await
}
// ---- Functions for reading the mailbox ---- // ---- Functions for reading the mailbox ----
/// Get a clone of the current UID Index of this mailbox /// Get a clone of the current UID Index of this mailbox
@ -199,6 +204,11 @@ impl MailboxInternal {
Ok(()) Ok(())
} }
async fn idle_sync(&mut self) -> Result<()> {
self.uid_index.idle_sync().await?;
Ok(())
}
// ---- Functions for reading the mailbox ---- // ---- Functions for reading the mailbox ----
async fn fetch_meta(&self, ids: &[UniqueIdent]) -> Result<Vec<MailMeta>> { async fn fetch_meta(&self, ids: &[UniqueIdent]) -> Result<Vec<MailMeta>> {