#![deny(unsafe_code)]
use roomrs_core::rusqlite::types::FromSql;
use roomrs_core::{Database, DatabaseInner, Error, FromRow, Params, Result, SyncHandle};
use std::sync::Arc;
pub use roomrs_core::rusqlite;
mod offload {
pub(crate) type Job = Box<dyn FnOnce() + Send + 'static>;
mod pool {
use super::Job;
use std::sync::LazyLock;
use std::sync::mpsc::{Sender, channel};
const MAX_WORKERS: usize = 1024;
fn effective_worker_count(env_val: Option<&str>, parallelism: usize) -> usize {
if let Some(v) = env_val {
if let Ok(n) = v.trim().parse::<usize>() {
if n > MAX_WORKERS {
log::warn!(
"ROOMRS_ASYNC_WORKERS={n} exceeds the cap; \
clamped to {MAX_WORKERS}"
);
return MAX_WORKERS;
}
if n > 0 {
return n;
}
}
}
parallelism.max(4)
}
fn worker_count() -> usize {
let parallelism = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
effective_worker_count(
std::env::var("ROOMRS_ASYNC_WORKERS").ok().as_deref(),
parallelism,
)
}
static POOL: LazyLock<Sender<Job>> = LazyLock::new(|| {
let (tx, rx) = channel::<Job>();
let rx = std::sync::Arc::new(std::sync::Mutex::new(rx));
for i in 0..worker_count() {
let rx = std::sync::Arc::clone(&rx);
let _ = std::thread::Builder::new()
.name(format!("roomrs-worker-{i}"))
.spawn(move || {
loop {
let job = {
rx.lock()
.expect(
"논리적 불가능: 락 구간은 recv뿐이고 사용자 코드는 \
catch_unwind로 격리되어 poisoned 될 수 없음",
)
.recv()
};
match job {
Ok(job) => {
if let Err(payload) =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(job))
{
let msg = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_owned())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| {
"<non-string panic payload>".to_owned()
});
let _ = std::panic::catch_unwind(move || {
log::warn!(
"async job panicked — isolated, \
worker continues: {msg}"
);
});
}
}
Err(_) => break, }
}
});
}
tx
});
pub(crate) fn spawn(job: Job) {
let _ = POOL.send(job);
}
#[cfg(test)]
mod tests {
use super::{MAX_WORKERS, effective_worker_count};
#[test]
fn default_is_max_of_4_and_parallelism() {
assert_eq!(effective_worker_count(None, 1), 4);
assert_eq!(effective_worker_count(None, 4), 4);
assert_eq!(effective_worker_count(None, 16), 16);
}
#[test]
fn zero_and_invalid_are_ignored() {
assert_eq!(effective_worker_count(Some("0"), 8), 8);
assert_eq!(effective_worker_count(Some("abc"), 8), 8);
assert_eq!(effective_worker_count(Some(""), 8), 8);
assert_eq!(effective_worker_count(Some("-3"), 8), 8);
}
#[test]
fn valid_value_is_used() {
assert_eq!(effective_worker_count(Some("2"), 8), 2);
assert_eq!(effective_worker_count(Some(" 32 "), 8), 32);
}
#[test]
fn huge_value_is_clamped_to_1024() {
assert_eq!(effective_worker_count(Some("1024"), 8), 1024);
assert_eq!(effective_worker_count(Some("1025"), 8), MAX_WORKERS);
assert_eq!(effective_worker_count(Some("999999999"), 8), MAX_WORKERS);
}
}
}
#[cfg(not(feature = "tokio"))]
pub(crate) use pool::spawn;
#[cfg(feature = "tokio")]
pub(crate) fn spawn(job: Job) {
match tokio::runtime::Handle::try_current() {
Ok(h) => drop(h.spawn_blocking(job)),
Err(_) => pool::spawn(job),
}
}
}
fn job_lost_error() -> Error {
Error::Internal("비동기 작업이 결과 없이 종료되었습니다(클로저 패닉 또는 워커 풀 소멸)".into())
}
async fn run_on_worker<R, F>(inner: Arc<DatabaseInner>, f: F) -> Result<R>
where
R: Send + 'static,
F: FnOnce(SyncHandle<'_>) -> Result<R> + Send + 'static,
{
let (tx, rx) = futures_channel::oneshot::channel::<Result<R>>();
offload::spawn(Box::new(move || {
let out = f(inner.sync_handle());
let _ = tx.send(out);
}));
rx.await.map_err(|_| job_lost_error())?
}
#[derive(Clone)]
pub struct AsyncHandle {
inner: Arc<DatabaseInner>,
}
impl AsyncHandle {
#[doc(hidden)]
pub fn from_database(db: &Database) -> Self {
Self {
inner: db.inner_arc(),
}
}
#[doc(hidden)]
pub fn run<R, F>(&self, f: F) -> impl Future<Output = Result<R>> + Send + use<R, F>
where
R: Send + 'static,
F: FnOnce(SyncHandle<'_>) -> Result<R> + Send + 'static,
{
run_on_worker(Arc::clone(&self.inner), f)
}
pub fn execute<S: Into<String>, P>(
&self,
sql: S,
params: P,
) -> impl Future<Output = Result<u64>> + Send + use<S, P>
where
P: Params + Send + 'static,
{
let sql = sql.into();
self.run(move |h| h.execute(&sql, params))
}
pub fn query_one<S: Into<String>, T, P>(
&self,
sql: S,
params: P,
) -> impl Future<Output = Result<T>> + Send + use<S, T, P>
where
T: FromRow + Send + 'static,
P: Params + Send + 'static,
{
let sql = sql.into();
self.run(move |h| h.query_one(&sql, params))
}
pub fn query_optional<S: Into<String>, T, P>(
&self,
sql: S,
params: P,
) -> impl Future<Output = Result<Option<T>>> + Send + use<S, T, P>
where
T: FromRow + Send + 'static,
P: Params + Send + 'static,
{
let sql = sql.into();
self.run(move |h| h.query_optional(&sql, params))
}
pub fn query_scalar<S: Into<String>, T, P>(
&self,
sql: S,
params: P,
) -> impl Future<Output = Result<T>> + Send + use<S, T, P>
where
T: FromSql + Send + 'static,
P: Params + Send + 'static,
{
let sql = sql.into();
self.run(move |h| h.query_scalar(&sql, params))
}
pub fn query_all<S: Into<String>, T, P>(
&self,
sql: S,
params: P,
) -> impl Future<Output = Result<Vec<T>>> + Send + use<S, T, P>
where
T: FromRow + Send + 'static,
P: Params + Send + 'static,
{
let sql = sql.into();
self.run(move |h| h.query_all(&sql, params))
}
pub fn transaction<R, F>(&self, f: F) -> impl Future<Output = Result<R>> + Send + use<R, F>
where
R: Send + 'static,
F: FnOnce(&mut roomrs_core::Tx<'_>) -> Result<R> + Send + 'static,
{
self.run(move |h| h.transaction(f))
}
}
#[cfg(feature = "live")]
impl AsyncHandle {
pub fn watch_all<T: FromRow + Clone + Send + 'static>(
&self,
sql: &str,
params: &[&dyn rusqlite::ToSql],
) -> roomrs_core::LiveQuery<Vec<T>> {
self.inner.__watch_all_dyn(sql, params)
}
pub fn watch_optional<T: FromRow + Clone + Send + 'static>(
&self,
sql: &str,
params: &[&dyn rusqlite::ToSql],
) -> roomrs_core::LiveQuery<Option<T>> {
self.inner.__watch_optional_dyn(sql, params)
}
pub fn watch_scalar<T: rusqlite::types::FromSql + Clone + Send + 'static>(
&self,
sql: &str,
params: &[&dyn rusqlite::ToSql],
) -> roomrs_core::LiveQuery<T> {
self.inner.__watch_scalar_dyn(sql, params)
}
}
#[cfg(feature = "live")]
impl roomrs_core::WatchContext for AsyncHandle {
fn ctx_watch_all_named<T: FromRow + Clone + Send + 'static>(
&self,
sql: &'static str,
params: Result<Vec<(String, rusqlite::types::Value)>>,
tables: &[&str],
) -> roomrs_core::LiveQuery<Vec<T>> {
self.inner.__watch_all_named(sql, params, tables)
}
fn ctx_watch_optional_named<T: FromRow + Clone + Send + 'static>(
&self,
sql: &'static str,
params: Result<Vec<(String, rusqlite::types::Value)>>,
tables: &[&str],
) -> roomrs_core::LiveQuery<Option<T>> {
self.inner.__watch_optional_named(sql, params, tables)
}
fn ctx_watch_scalar_named<T: rusqlite::types::FromSql + Clone + Send + 'static>(
&self,
sql: &'static str,
params: Result<Vec<(String, rusqlite::types::Value)>>,
tables: &[&str],
) -> roomrs_core::LiveQuery<T> {
self.inner.__watch_scalar_named(sql, params, tables)
}
}
impl roomrs_core::Execute for &AsyncHandle {
type Out<R: Send + 'static> =
std::pin::Pin<Box<dyn Future<Output = Result<R>> + Send + 'static>>;
fn run_all<T: FromRow + Send + 'static>(
self,
sql: String,
params: Vec<rusqlite::types::Value>,
) -> Self::Out<Vec<T>> {
Box::pin(self.query_all(sql, roomrs_core::params_from_iter(params)))
}
fn run_optional<T: FromRow + Send + 'static>(
self,
sql: String,
params: Vec<rusqlite::types::Value>,
) -> Self::Out<Option<T>> {
Box::pin(self.query_optional(sql, roomrs_core::params_from_iter(params)))
}
fn run_one<T: FromRow + Send + 'static>(
self,
sql: String,
params: Vec<rusqlite::types::Value>,
) -> Self::Out<T> {
Box::pin(self.query_one(sql, roomrs_core::params_from_iter(params)))
}
fn run_scalar(self, sql: String, params: Vec<rusqlite::types::Value>) -> Self::Out<i64> {
Box::pin(self.query_scalar(sql, roomrs_core::params_from_iter(params)))
}
fn fail<R: Send + 'static>(e: Error) -> Self::Out<R> {
Box::pin(async move { Err(e) })
}
}
pub trait BuildAsyncExt<T> {
fn build_async(self) -> impl Future<Output = Result<T>> + Send;
}
impl<T> BuildAsyncExt<T> for roomrs_core::DatabaseBuilder<T>
where
T: roomrs_core::DatabaseSpec + Send + 'static,
{
async fn build_async(self) -> Result<T> {
let (tx, rx) = futures_channel::oneshot::channel::<Result<T>>();
offload::spawn(Box::new(move || {
let _ = tx.send(self.build());
}));
rx.await.map_err(|_| job_lost_error())?
}
}