Skip to main content

surrealdb_engine_api/
session.rs

1//! The registry an engine keeps its sessions in.
2//!
3//! Session lifecycle travels on a [`SessionId`] channel of its own, separate
4//! from the requests that run under those sessions, and the SDK guarantees only
5//! that it announces a session before handing out the handle that uses it. A
6//! request can therefore reach an engine before the event registering its
7//! session has been applied.
8//!
9//! That makes a lookup which finds nothing ambiguous: the session may be one
10//! whose registration has not been applied *yet*, which is worth waiting for, or
11//! one that has *ended*, which is not. Waiting on the second never returns, so
12//! the registry remembers which sessions it has seen end, and answers for them
13//! rather than parking a request on a registration that has already come and
14//! gone.
15//!
16//! The payload is whatever the engine establishes for a session: the embedded
17//! engine publishes the state it keeps locally, and the gRPC engine publishes
18//! the identity the server minted for it.
19
20use std::collections::{HashMap, VecDeque};
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Mutex, MutexGuard};
23
24use tokio::sync::Notify;
25use uuid::Uuid;
26
27use crate::SessionError;
28
29/// How a session turned out: the payload the engine established for it, or why
30/// it never was.
31///
32/// The failure is the engine's own error type where it has one, so that what
33/// stopped a session being established survives to the request that goes on to
34/// need it -- a connection failure has to still read as one, or a caller cannot
35/// tell that reconnecting is what to do. The registry's own failures convert
36/// into it.
37pub type Established<T, E = SessionError> = Result<T, E>;
38
39/// How many ended sessions are remembered, so that a request naming one is told
40/// it has gone instead of waiting for it.
41///
42/// A request can only outlive its session by having been made from something
43/// that held a handle and then released it, so the window this has to cover is
44/// one request, not one session's lifetime. The bound is what keeps a
45/// connection that clones a handle per request from accumulating one record per
46/// clone for ever.
47const REMEMBERED_ENDED_SESSIONS: usize = 1024;
48
49/// One session's payload, and the signal that it has been established.
50pub struct SessionEntry<T, E = SessionError> {
51	ready: Notify,
52	outcome: Mutex<Option<Established<T, E>>>,
53}
54
55impl<T, E> Default for SessionEntry<T, E> {
56	fn default() -> Self {
57		Self {
58			ready: Notify::default(),
59			outcome: Mutex::new(None),
60		}
61	}
62}
63
64impl<T: Clone, E: Clone> SessionEntry<T, E> {
65	/// Waits until this session has been established, one way or the other.
66	///
67	/// Follows [`Notify`]'s race-free pattern: the notified future is created
68	/// *before* the readiness check, so a `notify_waiters()` landing between the
69	/// check and the await is not missed. The loop covers a spurious wake-up,
70	/// which would otherwise return before the session existed.
71	pub async fn wait_ready(&self) -> Established<T, E> {
72		loop {
73			let notified = self.ready.notified();
74			if let Some(outcome) = self.outcome() {
75				return outcome;
76			}
77			notified.await;
78		}
79	}
80
81	/// How establishing this session turned out, or `None` if it has not been
82	/// attempted yet. Unlike [`wait_ready`](Self::wait_ready) this does not wait
83	/// for the answer.
84	pub fn outcome(&self) -> Option<Established<T, E>> {
85		self.guard().clone()
86	}
87
88	/// Publishes the outcome of establishing this session.
89	///
90	/// A failure is published rather than swallowed so that waiters fail with
91	/// the reason the session was never established, instead of hanging.
92	pub fn publish(&self, outcome: Established<T, E>) {
93		*self.guard() = Some(outcome);
94		self.ready.notify_waiters();
95	}
96
97	fn guard(&self) -> MutexGuard<'_, Option<Established<T, E>>> {
98		self.outcome.lock().expect("session registry poisoned")
99	}
100}
101
102/// The sessions an engine is serving, keyed by the id the SDK gave them.
103pub struct SessionRegistry<T, E = SessionError> {
104	entries: Mutex<Entries<T, E>>,
105	/// Whether any further lifecycle event can still arrive.
106	///
107	/// Once the SDK has dropped its last handle no event ever will, so a session
108	/// that is not registered by then never will be, and a request for one must
109	/// fail rather than wait for a registration that cannot come.
110	closed: AtomicBool,
111}
112
113impl<T, E> Default for SessionRegistry<T, E> {
114	fn default() -> Self {
115		Self {
116			entries: Mutex::new(Entries::default()),
117			closed: AtomicBool::new(false),
118		}
119	}
120}
121
122struct Entries<T, E> {
123	live: HashMap<Uuid, Arc<SessionEntry<T, E>>>,
124	ended: VecDeque<Uuid>,
125}
126
127impl<T, E> Default for Entries<T, E> {
128	fn default() -> Self {
129		Self {
130			live: HashMap::new(),
131			ended: VecDeque::new(),
132		}
133	}
134}
135
136impl<T, E> Entries<T, E> {
137	fn end(&mut self, id: Uuid) -> Option<Arc<SessionEntry<T, E>>> {
138		self.ended.push_back(id);
139		if self.ended.len() > REMEMBERED_ENDED_SESSIONS {
140			self.ended.pop_front();
141		}
142		self.live.remove(&id)
143	}
144
145	fn has_ended(&self, id: Uuid) -> bool {
146		self.ended.contains(&id)
147	}
148}
149
150impl<T: Clone, E: Clone + From<SessionError>> SessionRegistry<T, E> {
151	fn map(&self) -> MutexGuard<'_, Entries<T, E>> {
152		self.entries.lock().expect("session registry poisoned")
153	}
154
155	/// The payload a request runs against, waiting for the session to be
156	/// registered if its lifecycle event has not been applied yet.
157	pub async fn resolve(&self, id: Uuid) -> Established<T, E> {
158		let entry = {
159			let mut entries = self.map();
160			// Deciding this here, under the same lock the lifecycle events are
161			// applied behind, is what keeps a request from waiting on a session
162			// that has already ended.
163			if !entries.live.contains_key(&id)
164				&& (entries.has_ended(id) || self.closed.load(Ordering::Acquire))
165			{
166				return Err(SessionError::NotFound(id).into());
167			}
168			Arc::clone(entries.live.entry(id).or_default())
169		};
170		entry.wait_ready().await
171	}
172
173	/// The entry a lifecycle event publishes into, creating it if a request is
174	/// not already waiting on one.
175	pub fn entry(&self, id: Uuid) -> Arc<SessionEntry<T, E>> {
176		Arc::clone(self.map().live.entry(id).or_default())
177	}
178
179	/// How a session stands right now, without waiting for one or registering
180	/// one.
181	///
182	/// This is what work that cannot wait resolves against — delivering a
183	/// notification, say, which has nowhere to go if its session is gone.
184	pub fn established(&self, id: Uuid) -> Option<Established<T, E>> {
185		self.map().live.get(&id).cloned()?.outcome()
186	}
187
188	/// Ends a session, failing anything waiting on it.
189	///
190	/// The failure is published before the session is forgotten, so a request
191	/// that outlived the handle it was made from fails rather than waiting for a
192	/// session that has just gone away.
193	pub fn end(&self, id: Uuid) -> Option<Established<T, E>> {
194		let entry = self.map().end(id)?;
195		let outcome = entry.outcome();
196		entry.publish(Err(SessionError::NotFound(id).into()));
197		outcome
198	}
199
200	/// Fails every session that has not been established, and every one asked
201	/// for from now on, because no further lifecycle event can arrive.
202	pub fn close(&self) {
203		self.closed.store(true, Ordering::Release);
204		for (id, entry) in self.map().live.iter() {
205			if entry.outcome().is_none() {
206				entry.publish(Err(SessionError::NotFound(*id).into()));
207			}
208		}
209	}
210}
211
212#[cfg(test)]
213mod tests {
214	use super::*;
215
216	fn registry() -> Arc<SessionRegistry<u8, SessionError>> {
217		Arc::new(SessionRegistry::default())
218	}
219
220	#[tokio::test]
221	async fn a_request_waits_for_a_session_that_is_still_being_registered() {
222		let sessions = registry();
223		let id = Uuid::new_v4();
224
225		let waiter = {
226			let sessions = Arc::clone(&sessions);
227			tokio::spawn(async move { sessions.resolve(id).await })
228		};
229		// Let the waiter reach the wait before the session exists.
230		tokio::task::yield_now().await;
231		sessions.entry(id).publish(Ok(7));
232
233		assert_eq!(
234			waiter.await.unwrap().expect("registered"),
235			7,
236			"a queued registration resolves the request"
237		);
238	}
239
240	/// Waiting only makes sense while a registration can still arrive. A session
241	/// that has ended is not going to be registered again, so a request naming
242	/// one has to be told rather than left waiting.
243	#[tokio::test]
244	async fn a_request_for_an_ended_session_fails() {
245		let sessions = registry();
246		let id = Uuid::new_v4();
247
248		sessions.entry(id).publish(Ok(7));
249		assert_eq!(sessions.resolve(id).await.expect("registered"), 7);
250
251		assert_eq!(
252			sessions.end(id).expect("a live session").expect("established"),
253			7,
254			"ending hands back what was established"
255		);
256		assert!(matches!(sessions.resolve(id).await, Err(SessionError::NotFound(_))));
257	}
258
259	/// A request already parked when the session ends is failed too, rather than
260	/// left waiting on an entry nothing will publish into again.
261	#[tokio::test]
262	async fn ending_a_session_fails_the_request_already_waiting_on_it() {
263		let sessions = registry();
264		let id = Uuid::new_v4();
265
266		let waiter = {
267			let sessions = Arc::clone(&sessions);
268			tokio::spawn(async move { sessions.resolve(id).await })
269		};
270		tokio::task::yield_now().await;
271		sessions.end(id);
272
273		assert!(matches!(waiter.await.unwrap(), Err(SessionError::NotFound(_))));
274	}
275
276	/// A session that could not be established fails the request that needed it
277	/// with the reason it could not, not with a summary of one: a caller
278	/// deciding whether to reconnect has to be able to tell a connection failure
279	/// from anything else.
280	#[tokio::test]
281	async fn the_reason_a_session_failed_reaches_the_request() {
282		let sessions: Arc<SessionRegistry<u8, surrealdb_types::Error>> =
283			Arc::new(SessionRegistry::default());
284		let id = Uuid::new_v4();
285
286		sessions.entry(id).publish(Err(surrealdb_types::Error::connection(
287			"the server went away".to_string(),
288			surrealdb_types::ConnectionError::ConnectionFailed,
289		)));
290
291		let error = sessions.resolve(id).await.expect_err("the session was never established");
292		assert!(error.is_connection(), "the failure must still read as a connection failure");
293	}
294
295	/// Once the SDK has dropped its last handle no lifecycle event can arrive,
296	/// so anything still waiting is waiting for good.
297	#[tokio::test]
298	async fn closing_fails_the_waiting_and_everything_after() {
299		let sessions = registry();
300		let waiting = Uuid::new_v4();
301
302		let waiter = {
303			let sessions = Arc::clone(&sessions);
304			tokio::spawn(async move { sessions.resolve(waiting).await })
305		};
306		tokio::task::yield_now().await;
307		sessions.close();
308
309		assert!(matches!(waiter.await.unwrap(), Err(SessionError::NotFound(_))));
310		assert!(matches!(sessions.resolve(Uuid::new_v4()).await, Err(SessionError::NotFound(_))));
311	}
312}