use std::sync::Arc;
use async_channel::{Receiver, Sender};
use surrealdb_core::dbs::Session;
use surrealdb_core::kvs::Datastore;
use surrealdb_datastore::Transaction;
use surrealdb_engine_api::{SessionError, SessionId, session_error_to_error};
use surrealdb_types::{Action, Error, HashMap, Notification, Variables};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::engine::kill_live_query;
use crate::spawn;
pub(crate) struct SessionState {
pub(crate) session: RwLock<Session>,
pub(crate) vars: RwLock<Variables>,
pub(crate) transactions: HashMap<Uuid, Arc<Transaction>>,
pub(crate) abandoned: HashMap<Uuid, Arc<Transaction>>,
pub(crate) live_queries: HashMap<Uuid, Option<Sender<Result<Notification, Error>>>>,
}
impl SessionState {
pub(crate) fn new(id: Uuid) -> Self {
let mut session = Session::default().with_rt(true);
session.id = Some(id);
Self {
session: RwLock::new(session),
vars: RwLock::new(Variables::default()),
transactions: HashMap::new(),
abandoned: HashMap::new(),
live_queries: HashMap::new(),
}
}
async fn cloned(&self, id: Uuid) -> Self {
let mut session = self.session.read().await.clone();
session.id = Some(id);
Self {
session: RwLock::new(session),
vars: RwLock::new(self.vars.read().await.clone()),
transactions: HashMap::new(),
abandoned: HashMap::new(),
live_queries: HashMap::new(),
}
}
}
pub(crate) type SessionRegistry = surrealdb_engine_api::SessionRegistry<Arc<SessionState>>;
pub(crate) async fn resolve(
sessions: &SessionRegistry,
id: Uuid,
) -> Result<Arc<SessionState>, Error> {
sessions.resolve(id).await.map_err(session_error_to_error)
}
trait ApplySession {
async fn apply(&self, event: SessionId);
}
impl ApplySession for SessionRegistry {
async fn apply(&self, event: SessionId) {
match event {
SessionId::Initial(id) => self.entry(id).publish(Ok(Arc::new(SessionState::new(id)))),
SessionId::Clone {
old,
new,
} => {
let outcome = match self.established(old) {
Some(Ok(state)) => Ok(Arc::new(state.cloned(new).await)),
Some(Err(error)) => Err(error),
None => Err(SessionError::NotFound(old)),
};
self.entry(new).publish(outcome);
}
SessionId::Drop(id) => {
self.end(id);
}
}
}
}
pub(crate) async fn run(
kvs: Arc<Datastore>,
sessions: Arc<SessionRegistry>,
session_rx: Receiver<SessionId>,
notifications: Option<Receiver<Notification>>,
) {
while let Ok(event) = session_rx.recv().await {
sessions.apply(event).await;
}
sessions.close();
if let Some(notifications) = notifications {
notifications.close();
}
kvs.shutdown().await.ok();
}
pub(crate) async fn pump(
kvs: Arc<Datastore>,
sessions: Arc<SessionRegistry>,
notifications: Receiver<Notification>,
) {
while let Ok(notification) = notifications.recv().await {
deliver(&kvs, &sessions, notification).await;
}
}
async fn deliver(kvs: &Arc<Datastore>, sessions: &SessionRegistry, notification: Notification) {
let Some(session_id) = notification.session.map(|x| x.into_inner()) else {
return;
};
let live_query_id = notification.id.into_inner();
let state = match sessions.established(session_id) {
Some(Ok(state)) => state,
Some(Err(error)) => {
warn!(
"Failed to find session '{session_id:?}' for live query '{live_query_id}'; {error:?}"
);
return;
}
None => {
let error = session_error_to_error(SessionError::NotFound(session_id));
warn!(
"Failed to find session '{session_id:?}' for live query '{live_query_id}'; {error}"
);
return;
}
};
let ended = matches!(notification.action, Action::Killed);
let registration = match ended {
true => state.live_queries.take(&live_query_id),
false => state.live_queries.get(&live_query_id),
};
let sender = match registration {
Some(Some(sender)) => sender,
Some(None) => return,
None => {
if !ended {
warn!("Failed to find live query '{live_query_id}' for session '{session_id:?}'");
}
return;
}
};
let kvs = Arc::clone(kvs);
spawn(async move {
if sender.send(Ok(notification)).await.is_err() && !ended {
state.live_queries.remove(&live_query_id);
let vars = state.vars.read().await.clone();
let session = state.session.read().await.clone();
if let Err(error) = kill_live_query(&kvs, live_query_id, &session, vars).await {
warn!("Failed to kill live query '{live_query_id}'; {error}");
}
}
});
}
#[cfg(test)]
mod tests {
use surrealdb_types::Value;
use super::*;
fn registry() -> Arc<SessionRegistry> {
Arc::new(SessionRegistry::default())
}
#[test_log::test(tokio::test)]
async fn clone_carries_the_original_session_forward() {
let sessions = registry();
let old = Uuid::new_v4();
let new = Uuid::new_v4();
sessions.apply(SessionId::Initial(old)).await;
sessions
.resolve(old)
.await
.unwrap()
.vars
.write()
.await
.insert("a".to_string(), Value::Bool(true));
sessions
.apply(SessionId::Clone {
old,
new,
})
.await;
let cloned = sessions.resolve(new).await.expect("the clone is registered");
assert_eq!(cloned.session.read().await.id, Some(new));
assert_eq!(cloned.vars.read().await.get("a"), Some(&Value::Bool(true)));
}
#[test_log::test(tokio::test)]
async fn clone_of_an_unknown_session_resolves_to_not_found() {
let sessions = registry();
let new = Uuid::new_v4();
sessions
.apply(SessionId::Clone {
old: Uuid::new_v4(),
new,
})
.await;
assert!(sessions.resolve(new).await.is_err());
}
#[test_log::test(tokio::test)]
async fn a_request_waits_for_a_session_that_is_still_being_registered() {
let sessions = registry();
let id = Uuid::new_v4();
let waiter = {
let sessions = Arc::clone(&sessions);
tokio::spawn(async move { sessions.resolve(id).await.is_ok() })
};
tokio::task::yield_now().await;
sessions.apply(SessionId::Initial(id)).await;
assert!(waiter.await.unwrap(), "a queued registration must resolve the request");
}
#[cfg(feature = "kv-mem")]
#[test_log::test(tokio::test)]
async fn a_request_fails_once_no_further_session_can_arrive() {
let kvs = Datastore::new("memory").await.unwrap();
let sessions = registry();
let (session_tx, session_rx) = async_channel::unbounded();
let id = Uuid::new_v4();
let waiter = {
let sessions = Arc::clone(&sessions);
tokio::spawn(async move { sessions.resolve(id).await })
};
tokio::task::yield_now().await;
drop(session_tx);
run(kvs, Arc::clone(&sessions), session_rx, None).await;
assert!(
waiter.await.unwrap().is_err(),
"a parked request must be failed, not left waiting"
);
assert!(
sessions.resolve(Uuid::new_v4()).await.is_err(),
"a request arriving after the last handle went away must fail immediately"
);
}
#[test_log::test(tokio::test)]
async fn a_request_for_a_dropped_session_fails() {
let sessions = registry();
let id = Uuid::new_v4();
sessions.apply(SessionId::Initial(id)).await;
let parked = {
let sessions = Arc::clone(&sessions);
tokio::spawn(async move { sessions.resolve(id).await })
};
assert!(parked.await.unwrap().is_ok(), "the session is registered");
sessions.apply(SessionId::Drop(id)).await;
assert!(sessions.resolve(id).await.is_err(), "a dropped session must not be waited for");
}
#[cfg(feature = "kv-mem")]
#[test_log::test(tokio::test)]
async fn a_killed_notification_takes_the_registration_with_it() {
let kvs = Datastore::new("memory").await.unwrap();
let sessions = registry();
let session = Uuid::now_v7();
sessions.apply(SessionId::Initial(session)).await;
let state = sessions.resolve(session).await.unwrap();
let live = Uuid::now_v7();
let (sender, subscriber) = async_channel::unbounded();
state.live_queries.insert(live, Some(sender));
deliver(
&kvs,
&sessions,
Notification::new(
live.into(),
Some(session.into()),
Action::Killed,
Value::None,
Value::None,
),
)
.await;
assert!(
subscriber.recv().await.is_ok(),
"the subscriber is still owed the end of its subscription"
);
assert!(
state.live_queries.get(&live).is_none(),
"the registration outlived the subscription it names"
);
}
#[cfg(feature = "kv-mem")]
#[test_log::test(tokio::test)]
async fn a_change_notification_leaves_the_registration_in_place() {
let kvs = Datastore::new("memory").await.unwrap();
let sessions = registry();
let session = Uuid::now_v7();
sessions.apply(SessionId::Initial(session)).await;
let state = sessions.resolve(session).await.unwrap();
let live = Uuid::now_v7();
let (sender, subscriber) = async_channel::unbounded();
state.live_queries.insert(live, Some(sender));
deliver(
&kvs,
&sessions,
Notification::new(
live.into(),
Some(session.into()),
Action::Create,
Value::None,
Value::None,
),
)
.await;
assert!(subscriber.recv().await.is_ok(), "the change reaches the subscriber");
assert!(
matches!(state.live_queries.get(&live), Some(Some(_))),
"the subscription is still running and still the session's"
);
}
}