use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use mongreldb_protocol::prepared::{PreparedStatementBinding, StatementId};
use mongreldb_protocol::request::{AuthenticatedIdentity, IsolationLevel, SessionId};
use mongreldb_protocol::session::{Session, TransactionState};
use mongreldb_query::MongrelSession;
use mongreldb_types::hlc::HlcTimestamp;
use mongreldb_types::ids::{DatabaseId, TransactionId};
pub struct SessionEntry {
session: std::sync::RwLock<Arc<MongrelSession>>,
pub owner: String,
last_used: std::sync::Mutex<Instant>,
pub lock: tokio::sync::Mutex<()>,
closed: AtomicBool,
record: std::sync::Mutex<Session>,
prepared_names: std::sync::Mutex<BTreeMap<String, StatementId>>,
}
impl SessionEntry {
pub fn session(&self) -> Arc<MongrelSession> {
self.session
.read()
.unwrap_or_else(|error| error.into_inner())
.clone()
}
pub(crate) fn replace_session(&self, session: MongrelSession) {
*self
.session
.write()
.unwrap_or_else(|error| error.into_inner()) = Arc::new(session);
}
pub(crate) fn touch(&self) {
if let Ok(mut t) = self.last_used.lock() {
*t = Instant::now();
}
if let Ok(mut record) = self.record.lock() {
record.last_activity_unix_micros = now_unix_micros();
}
}
pub(crate) fn is_closed(&self) -> bool {
self.closed.load(Ordering::Acquire)
}
fn mark_closed(&self) {
self.closed.store(true, Ordering::Release);
}
fn idle_for_at_least(&self, timeout: Duration) -> bool {
self.last_used
.lock()
.map(|t| t.elapsed() >= timeout)
.unwrap_or(false)
}
pub fn protocol_record(&self) -> Session {
self.record
.lock()
.map(|record| record.clone())
.unwrap_or_else(|error| error.into_inner().clone())
}
pub(crate) fn sync_record_after_request(&self, commit: Option<HlcTimestamp>) {
let Ok(mut record) = self.record.lock() else {
return;
};
record.last_activity_unix_micros = now_unix_micros();
let staging = self.session().staged_sql_operation_count().is_some();
match (&record.transaction_state, staging) {
(TransactionState::Idle, true) => {
record.transaction_state = TransactionState::Active {
transaction_id: TransactionId::new_random(),
isolation: IsolationLevel::Snapshot,
};
}
(TransactionState::Active { .. }, false) => {
record.transaction_state = TransactionState::Idle;
}
_ => {}
}
if let Some(commit_ts) = commit {
record.read_your_writes_token = Some(commit_ts);
}
}
pub(crate) fn allocate_statement_id(&self) -> StatementId {
let record = self
.record
.lock()
.unwrap_or_else(|error| error.into_inner());
let next = record
.prepared_statements
.keys()
.next_back()
.map_or(1, |id| id.get().saturating_add(1));
StatementId::new(next)
}
pub(crate) fn prepared_binding(
&self,
name: &str,
) -> Option<(StatementId, PreparedStatementBinding)> {
let names = self
.prepared_names
.lock()
.unwrap_or_else(|error| error.into_inner());
let statement_id = names.get(name).copied()?;
let record = self
.record
.lock()
.unwrap_or_else(|error| error.into_inner());
record
.prepared_statements
.get(&statement_id)
.cloned()
.map(|binding| (statement_id, binding))
}
pub(crate) fn insert_prepared_binding(&self, name: String, binding: PreparedStatementBinding) {
self.prepared_names
.lock()
.unwrap_or_else(|error| error.into_inner())
.insert(name, binding.statement_id);
self.record
.lock()
.unwrap_or_else(|error| error.into_inner())
.prepared_statements
.insert(binding.statement_id, binding);
}
pub(crate) fn remove_prepared_binding(&self, name: &str) -> Option<StatementId> {
let statement_id = self
.prepared_names
.lock()
.unwrap_or_else(|error| error.into_inner())
.remove(name)?;
self.record
.lock()
.unwrap_or_else(|error| error.into_inner())
.prepared_statements
.remove(&statement_id);
Some(statement_id)
}
#[cfg(test)]
pub(crate) fn prepared_statement_count(&self) -> usize {
self.record
.lock()
.map(|record| record.prepared_statements.len())
.unwrap_or(0)
}
}
pub(crate) fn now_unix_micros() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_micros()
.min(u128::from(u64::MAX)) as u64
}
pub struct SessionStore {
sessions: std::sync::Mutex<HashMap<String, Arc<SessionEntry>>>,
max_sessions: usize,
idle_timeout: Duration,
database_id: DatabaseId,
}
impl SessionStore {
pub fn new(max_sessions: usize, idle_timeout: Duration) -> Self {
Self::new_with_database_id(max_sessions, idle_timeout, DatabaseId::new_random())
}
pub fn new_with_database_id(
max_sessions: usize,
idle_timeout: Duration,
database_id: DatabaseId,
) -> Self {
Self {
sessions: std::sync::Mutex::new(HashMap::new()),
max_sessions: max_sessions.max(1),
idle_timeout,
database_id,
}
}
pub fn database_id(&self) -> DatabaseId {
self.database_id
}
pub fn create(&self, session: MongrelSession, owner: String) -> Option<String> {
self.create_with_identity(session, owner, AuthenticatedIdentity::Credentialless)
}
pub fn create_with_identity(
&self,
session: MongrelSession,
owner: String,
principal: AuthenticatedIdentity,
) -> Option<String> {
let mut guard = self.sessions.lock().ok()?;
if guard.len() >= self.max_sessions {
return None;
}
let token = random_token()?;
let session_id = SessionId::from_str(&token).unwrap_or(SessionId::ZERO);
let record = Session::new(session_id, principal, self.database_id, now_unix_micros());
guard.insert(
token.clone(),
Arc::new(SessionEntry {
session: std::sync::RwLock::new(Arc::new(session)),
owner,
last_used: std::sync::Mutex::new(Instant::now()),
lock: tokio::sync::Mutex::new(()),
closed: AtomicBool::new(false),
record: std::sync::Mutex::new(record),
prepared_names: std::sync::Mutex::new(BTreeMap::new()),
}),
);
Some(token)
}
pub fn get(&self, token: &str, owner: &str) -> Option<Arc<SessionEntry>> {
let guard = self.sessions.lock().ok()?;
let entry = guard.get(token)?;
if entry.owner != owner || entry.is_closed() {
return None;
}
Some(Arc::clone(entry))
}
pub fn close(&self, token: &str, owner: &str) -> bool {
self.take_for_close(token, owner).is_some()
}
pub(crate) fn take_for_close(&self, token: &str, owner: &str) -> Option<Arc<SessionEntry>> {
if let Ok(mut guard) = self.sessions.lock() {
if let Some(entry) = guard.get(token) {
if entry.owner != owner {
return None;
}
entry.mark_closed();
}
return guard.remove(token);
}
None
}
pub fn sweep_idle(&self) -> usize {
let Ok(mut guard) = self.sessions.lock() else {
return 0;
};
let timeout = self.idle_timeout;
let to_evict: Vec<String> = guard
.iter()
.filter_map(|(token, entry)| {
if entry.idle_for_at_least(timeout)
&& entry.session().query_registry().active_for_session(token) == 0
&& entry.lock.try_lock().is_ok()
{
Some(token.clone())
} else {
None
}
})
.collect();
let count = to_evict.len();
for token in &to_evict {
if let Some(entry) = guard.remove(token) {
entry.mark_closed();
}
}
count
}
pub fn len(&self) -> usize {
self.sessions.lock().map(|g| g.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub(crate) fn close_all(&self) {
if let Ok(mut guard) = self.sessions.lock() {
for entry in guard.values() {
entry.mark_closed();
}
guard.clear();
}
}
}
pub fn spawn_session_reaper(store: Arc<SessionStore>) {
std::thread::Builder::new()
.name("mongreldb-session-reaper".into())
.spawn(move || loop {
std::thread::sleep(Duration::from_secs(30));
let evicted = store.sweep_idle();
if evicted > 0 {
eprintln!("[session-reaper] evicted {evicted} idle session(s)");
}
})
.expect("spawn session-reaper");
}
fn random_token() -> Option<String> {
let bytes = read_urandom(16)?;
let mut n = 0u128;
for &b in &bytes {
n = (n << 8) | b as u128;
}
Some(format!("{n:032x}"))
}
fn read_urandom(n: usize) -> Option<Vec<u8>> {
use std::io::Read;
let mut f = std::fs::File::open("/dev/urandom").ok()?;
let mut buf = vec![0u8; n];
f.read_exact(&mut buf).ok()?;
Some(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use mongreldb_core::Database;
use mongreldb_query::{RegisteredQueryGuard, SqlQueryOptions};
use tempfile::tempdir;
fn make_session() -> MongrelSession {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
std::mem::forget(dir);
MongrelSession::open(db).unwrap()
}
#[test]
fn create_and_get_roundtrip() {
let store = SessionStore::new(8, Duration::from_secs(60));
let token = store.create(make_session(), "alice".into()).unwrap();
assert!(store.get(&token, "alice").is_some());
assert!(store.get(&token, "eve").is_none());
assert_eq!(store.len(), 1);
}
#[test]
fn close_removes_session() {
let store = SessionStore::new(8, Duration::from_secs(60));
let token = store.create(make_session(), "alice".into()).unwrap();
assert!(store.close(&token, "alice"));
assert!(store.get(&token, "alice").is_none());
assert!(store.is_empty());
let t2 = store.create(make_session(), "bob".into()).unwrap();
assert!(!store.close(&t2, "alice"));
assert_eq!(store.len(), 1);
}
#[test]
fn capacity_limit_rejects_new_sessions() {
let store = SessionStore::new(1, Duration::from_secs(60));
assert!(store.create(make_session(), "a".into()).is_some());
assert!(store.create(make_session(), "b".into()).is_none());
assert_eq!(store.len(), 1);
}
#[test]
fn sweep_idle_evicts_stale_sessions() {
let store = SessionStore::new(8, Duration::from_millis(1));
let token = store.create(make_session(), "alice".into()).unwrap();
assert_eq!(store.len(), 1);
std::thread::sleep(Duration::from_millis(20));
let evicted = store.sweep_idle();
assert_eq!(evicted, 1);
assert!(store.get(&token, "alice").is_none());
assert!(store.is_empty());
}
#[test]
fn sweep_idle_keeps_active_queries() {
let store = SessionStore::new(8, Duration::from_millis(1));
let token = store.create(make_session(), "alice".into()).unwrap();
let entry = store.get(&token, "alice").unwrap();
let query = entry
.session()
.register_query(SqlQueryOptions {
session_id: Some(token.clone()),
..SqlQueryOptions::default()
})
.unwrap();
let query = RegisteredQueryGuard::new(query);
std::thread::sleep(Duration::from_millis(20));
assert_eq!(store.sweep_idle(), 0);
assert!(store.get(&token, "alice").is_some());
drop(query);
assert_eq!(store.sweep_idle(), 1);
assert!(store.is_empty());
}
#[test]
fn protocol_record_carries_identity_database_and_session_id() {
let database_id = DatabaseId::new_random();
let store = SessionStore::new_with_database_id(8, Duration::from_secs(60), database_id);
let identity = AuthenticatedIdentity::CatalogUser {
username: "alice".to_owned(),
user_id: 42,
created_version: 7,
};
let token = store
.create_with_identity(make_session(), "alice".into(), identity.clone())
.unwrap();
let entry = store.get(&token, "alice").unwrap();
let record = entry.protocol_record();
assert_eq!(record.session_id, SessionId::from_str(&token).unwrap());
assert_eq!(record.principal, identity);
assert_eq!(record.current_database, database_id);
assert_eq!(record.transaction_state, TransactionState::Idle);
assert!(record.prepared_statements.is_empty());
assert!(record.settings.is_empty());
assert_eq!(record.read_your_writes_token, None);
assert!(record.last_activity_unix_micros > 0);
assert_eq!(store.database_id(), database_id);
}
#[test]
fn sync_record_tracks_commit_and_activity() {
let store = SessionStore::new(8, Duration::from_secs(60));
let token = store.create(make_session(), "alice".into()).unwrap();
let entry = store.get(&token, "alice").unwrap();
let before = entry.protocol_record().last_activity_unix_micros;
std::thread::sleep(Duration::from_millis(2));
let commit_ts = HlcTimestamp {
physical_micros: now_unix_micros().saturating_sub(1_000),
logical: 3,
node_tiebreaker: 0,
};
entry.sync_record_after_request(Some(commit_ts));
let record = entry.protocol_record();
assert!(record.last_activity_unix_micros >= before);
assert_eq!(
record.read_your_writes_token,
Some(commit_ts),
"a committed request must advance the read-your-writes token to the commit timestamp"
);
assert_eq!(record.transaction_state, TransactionState::Idle);
entry.sync_record_after_request(None);
assert_eq!(
entry.protocol_record().read_your_writes_token,
Some(commit_ts)
);
}
#[test]
fn prepared_bindings_are_tracked_by_name_and_id() {
let store = SessionStore::new(8, Duration::from_secs(60));
let token = store.create(make_session(), "alice".into()).unwrap();
let entry = store.get(&token, "alice").unwrap();
let first = entry.allocate_statement_id();
assert_eq!(first, StatementId::new(1));
let mut binding = PreparedStatementBinding {
statement_id: first,
sql: "SELECT 1".to_owned(),
parameter_types: vec![],
catalog_version: mongreldb_types::ids::MetadataVersion::new(3),
schema_versions: BTreeMap::new(),
feature_set: Default::default(),
};
entry.insert_prepared_binding("stmt_a".to_owned(), binding.clone());
assert_eq!(entry.prepared_statement_count(), 1);
assert_eq!(
entry.prepared_binding("stmt_a"),
Some((first, binding.clone()))
);
let second = entry.allocate_statement_id();
assert_eq!(second, StatementId::new(2));
binding.statement_id = second;
entry.insert_prepared_binding("stmt_b".to_owned(), binding);
assert_eq!(entry.remove_prepared_binding("stmt_a"), Some(first));
assert_eq!(entry.prepared_binding("stmt_a"), None);
assert_eq!(entry.prepared_statement_count(), 1);
assert_eq!(entry.protocol_record().prepared_statements.len(), 1);
assert_eq!(entry.remove_prepared_binding("stmt_a"), None);
}
}