use std::{
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use super::{
DatafusionAlgeonFlightSqlService, Error, ErrorCode, LogicalPlan, MlForestModelDigest, StatementExecutionDomain,
ownership::Owner, verify_sql_statement_namespace, verify_update_ddl,
};
use crate::server::adapter_error::Result;
#[derive(Debug)]
pub(super) struct PreparedStatementEntry {
pub(super) sql: String,
pub(super) no_result_ddl: bool,
pub(super) execution_domain: StatementExecutionDomain,
pub(super) forest_model_digests: Vec<MlForestModelDigest>,
pub(super) executed: AtomicBool,
}
impl PreparedStatementEntry {
pub(super) fn new(
sql: String,
no_result_ddl: bool,
execution_domain: StatementExecutionDomain,
forest_model_digests: Vec<MlForestModelDigest>,
) -> Self {
Self { sql, no_result_ddl, execution_domain, forest_model_digests, executed: AtomicBool::new(false) }
}
pub(super) fn ddl_already_executed(&self) -> bool {
self.no_result_ddl && self.executed.load(Ordering::Relaxed)
}
pub(super) fn mark_executed(&self) {
if self.no_result_ddl {
self.executed.store(true, Ordering::Relaxed);
}
}
}
const MAX_PREPARED_STATEMENTS: usize = 1024;
const MAX_PREPARED_STATEMENTS_PER_PRINCIPAL: usize = MAX_PREPARED_STATEMENTS / 4;
#[derive(Debug, Default)]
pub(super) struct PreparedStatementStore {
state: Mutex<PreparedStatementState>,
}
#[derive(Debug, Default)]
struct PreparedStatementState {
entries: HashMap<String, Arc<PreparedStatementEntry>>,
per_owner: HashMap<Owner, usize>,
}
impl PreparedStatementStore {
pub(super) fn insert(
&self,
handle: String,
owner: Owner,
entry: PreparedStatementEntry,
) -> std::result::Result<(), PreparedStatementLimit> {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if state.entries.len() >= MAX_PREPARED_STATEMENTS {
return Err(PreparedStatementLimit::Global);
}
if owner != Owner::Anonymous {
let owned = state.per_owner.get(&owner).copied().unwrap_or(0);
if owned >= MAX_PREPARED_STATEMENTS_PER_PRINCIPAL {
return Err(PreparedStatementLimit::Principal);
}
}
if state.entries.insert(handle, Arc::new(entry)).is_none() {
*state.per_owner.entry(owner).or_insert(0) += 1;
}
Ok(())
}
pub(super) fn get(&self, handle: &str) -> Option<Arc<PreparedStatementEntry>> {
let state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
state.entries.get(handle).map(Arc::clone)
}
pub(super) fn remove(&self, handle: &str, owner: &Owner) -> bool {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if state.entries.remove(handle).is_none() {
return false;
}
if let std::collections::hash_map::Entry::Occupied(mut count) = state.per_owner.entry(owner.clone()) {
*count.get_mut() -= 1;
if *count.get() == 0 {
count.remove();
}
}
true
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner).entries.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PreparedStatementLimit {
Global,
Principal,
}
impl PreparedStatementLimit {
pub(super) const fn limit(self) -> usize {
match self {
Self::Global => MAX_PREPARED_STATEMENTS,
Self::Principal => MAX_PREPARED_STATEMENTS_PER_PRINCIPAL,
}
}
pub(super) const fn message(self) -> &'static str {
match self {
Self::Global => "too many live prepared statements on this server",
Self::Principal => "too many live prepared statements for this user",
}
}
}
impl DatafusionAlgeonFlightSqlService {
pub(super) fn verify_prepared_statement_plan(&self, plan: &LogicalPlan) -> Result<()> {
match plan {
LogicalPlan::Ddl(ddl) => verify_update_ddl(
ddl,
"algeon_datafusion.server.flight.do_action_create_prepared_statement.verify_plan",
),
LogicalPlan::Dml(_) | LogicalPlan::Copy(_) => Err(Error::new(
ErrorCode::UnsupportedOperator,
"algeon_datafusion.server.flight.do_action_create_prepared_statement.verify_plan",
"Flight SQL prepared DML and COPY statements are not supported",
)),
LogicalPlan::Statement(statement) => verify_sql_statement_namespace(
statement,
"algeon_datafusion.server.flight.do_action_create_prepared_statement.verify_plan",
),
_ => Ok(()),
}
}
pub(super) fn prepared_statement_has_result_set(&self, plan: &LogicalPlan) -> bool {
!matches!(plan, LogicalPlan::Ddl(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::auth::Principal;
fn owner(name: &str) -> Owner {
Owner::from_principal(Some(&Principal { username: name.to_string() }))
}
fn entry() -> PreparedStatementEntry {
PreparedStatementEntry::new("SELECT 1".to_string(), false, StatementExecutionDomain::Data, Vec::new())
}
fn fill(store: &PreparedStatementStore, owner: &Owner, count: usize) {
for index in 0..count {
store.insert(format!("{owner:?}-{index}"), owner.clone(), entry()).expect("below both caps");
}
}
#[test]
fn principal_cap_does_not_starve_other_principals() {
let store = PreparedStatementStore::default();
let hog = owner("mallory");
fill(&store, &hog, MAX_PREPARED_STATEMENTS_PER_PRINCIPAL);
assert_eq!(
store.insert("one-too-many".to_string(), hog, entry()).unwrap_err(),
PreparedStatementLimit::Principal
);
store.insert("alice-0".to_string(), owner("alice"), entry()).expect("another principal still has quota");
}
#[test]
fn closing_a_handle_returns_quota_to_its_owner() {
let store = PreparedStatementStore::default();
let alice = owner("alice");
fill(&store, &alice, MAX_PREPARED_STATEMENTS_PER_PRINCIPAL);
assert!(store.insert("blocked".to_string(), alice.clone(), entry()).is_err());
assert!(store.remove(&format!("{alice:?}-0"), &alice), "a live handle is removed");
store.insert("reused".to_string(), alice, entry()).expect("the released slot is reusable");
}
#[test]
fn global_cap_bounds_the_anonymous_owner() {
let store = PreparedStatementStore::default();
for index in 0..MAX_PREPARED_STATEMENTS {
store.insert(format!("anon-{index}"), Owner::Anonymous, entry()).expect("below the global cap");
}
assert_eq!(
store.insert("overflow".to_string(), Owner::Anonymous, entry()).unwrap_err(),
PreparedStatementLimit::Global
);
}
#[test]
fn removing_an_unknown_handle_is_a_no_op() {
let store = PreparedStatementStore::default();
assert!(!store.remove("never-created", &owner("alice")));
assert_eq!(store.len(), 0);
}
#[test]
fn concurrent_creates_cannot_exceed_the_cap() {
let store = Arc::new(PreparedStatementStore::default());
let start = Arc::new(std::sync::Barrier::new(8));
let accepted = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let threads: Vec<_> = (0..8)
.map(|thread| {
let store = Arc::clone(&store);
let start = Arc::clone(&start);
let accepted = Arc::clone(&accepted);
std::thread::spawn(move || {
start.wait();
for index in 0..MAX_PREPARED_STATEMENTS {
if store.insert(format!("{thread}-{index}"), Owner::Anonymous, entry()).is_ok() {
accepted.fetch_add(1, Ordering::Relaxed);
}
}
})
})
.collect();
for thread in threads {
thread.join().expect("worker thread");
}
assert_eq!(accepted.load(Ordering::Relaxed), MAX_PREPARED_STATEMENTS);
assert_eq!(store.len(), MAX_PREPARED_STATEMENTS);
}
}