use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use mlua::{Lua, Value as LuaValue};
use serde_json::Value as JsonValue;
use tokio::sync::broadcast;
use crate::error::DbError;
use crate::scripting::channel_manager::ChannelManager;
use crate::storage::StorageEngine;
use crate::stream::StreamManager;
use super::conversion::lua_to_json_value;
use super::types::{Script, ScriptContext, ScriptResult, ScriptStats};
pub mod cache;
pub mod globals;
pub mod pool;
pub mod repl;
pub mod script_index;
pub mod websocket;
pub use cache::ScriptCache;
pub use pool::LuaPool;
pub use script_index::ScriptIndex;
pub fn lua_runtime_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| !env_flag_is_set(std::env::var("SOLIDB_NO_LUA").ok().as_deref()))
}
pub(crate) fn env_flag_is_set(value: Option<&str>) -> bool {
matches!(
value,
Some(v) if v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes")
)
}
pub fn lua_disabled_error() -> DbError {
DbError::OperationNotSupported(
"Lua is disabled (--no-lua / SOLIDB_NO_LUA). Custom scripts, services, and the Lua REPL are not available.".to_string(),
)
}
fn install_deadline_hook(lua: &Lua) {
static TIMEOUT_SECS: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
let timeout = *TIMEOUT_SECS.get_or_init(|| {
std::env::var("SOLIDB_LUA_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(30)
});
if timeout == 0 {
return;
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout);
let _ = lua.set_hook(
mlua::HookTriggers::new().every_nth_instruction(50_000),
move |_lua, _debug| {
if std::time::Instant::now() > deadline {
Err(mlua::Error::RuntimeError(format!(
"script exceeded execution time limit ({}s)",
timeout
)))
} else {
Ok(mlua::VmState::Continue)
}
},
);
}
pub struct ScriptEngine {
pub(crate) storage: Arc<StorageEngine>,
pub(crate) queue_notifier: Option<broadcast::Sender<()>>,
pub(crate) stream_manager: Option<Arc<StreamManager>>,
pub(crate) channel_manager: Option<Arc<ChannelManager>>,
pub(crate) stats: Arc<ScriptStats>,
pub(crate) lua_pool: Option<Arc<LuaPool>>,
pub(crate) script_cache: Option<Arc<ScriptCache>>,
}
impl ScriptEngine {
pub fn new(storage: Arc<StorageEngine>, stats: Arc<ScriptStats>) -> Self {
Self {
storage,
queue_notifier: None,
stream_manager: None,
channel_manager: None,
stats,
lua_pool: None,
script_cache: None,
}
}
pub fn with_queue_notifier(mut self, notifier: broadcast::Sender<()>) -> Self {
self.queue_notifier = Some(notifier);
self
}
pub fn with_stream_manager(mut self, manager: Arc<StreamManager>) -> Self {
self.stream_manager = Some(manager);
self
}
pub fn with_channel_manager(mut self, manager: Arc<ChannelManager>) -> Self {
self.channel_manager = Some(manager);
self
}
pub fn with_lua_pool(mut self, pool: Arc<LuaPool>) -> Self {
self.lua_pool = Some(pool);
self
}
pub fn with_script_cache(mut self, cache: Arc<ScriptCache>) -> Self {
self.script_cache = Some(cache);
self
}
pub async fn execute(
&self,
script: &Script,
db_name: &str,
context: &ScriptContext,
) -> Result<ScriptResult, DbError> {
if !lua_runtime_enabled() {
return Err(lua_disabled_error());
}
self.stats.active_scripts.fetch_add(1, Ordering::Relaxed);
self.stats
.total_scripts_executed
.fetch_add(1, Ordering::Relaxed);
struct ActiveScriptGuard<'a>(&'a ScriptStats);
impl Drop for ActiveScriptGuard<'_> {
fn drop(&mut self) {
self.0.active_scripts.fetch_sub(1, Ordering::Relaxed);
}
}
let _guard = ActiveScriptGuard(&self.stats);
if let Some(ref pool) = self.lua_pool {
self.execute_with_pool(pool, script, db_name, context).await
} else {
self.execute_without_pool(script, db_name, context).await
}
}
async fn execute_with_pool(
&self,
pool: &Arc<LuaPool>,
script: &Script,
db_name: &str,
context: &ScriptContext,
) -> Result<ScriptResult, DbError> {
let pool_guard = pool.acquire();
let needs = if let Some(ref cache) = self.script_cache {
cache.get_or_analyze_needs(&script.key, &script.code)
} else {
globals::ScriptNeeds::analyze(&script.code)
};
pool_guard.with_lua(|lua| {
if needs.any() {
let has_static_globals = lua
.globals()
.get::<bool>("__solidb_static_initialized")
.unwrap_or(false);
if has_static_globals {
globals::setup_request_globals_selective(
self,
lua,
db_name,
context,
Some((&script.key, &script.name)),
Some(&needs),
)?;
} else {
self.setup_lua_globals(
lua,
db_name,
context,
Some((&script.key, &script.name)),
)?;
}
}
let bytecode = if let Some(ref cache) = self.script_cache {
cache
.get_or_compile(&script.key, &script.code, |code| {
let chunk = lua.load(code);
let func = chunk.into_function()?;
Ok(func.dump(false))
})
.map_err(|e| {
DbError::InternalError(format!("Bytecode compilation error: {}", e))
})?
} else {
let chunk = lua.load(&script.code);
let func = chunk.into_function().map_err(|e| {
DbError::InternalError(format!("Script compilation error: {}", e))
})?;
func.dump(false)
};
install_deadline_hook(lua);
let chunk = lua.load(&bytecode[..]);
let lua_result = chunk.eval::<LuaValue>();
lua.remove_hook();
let lua_result =
lua_result.map_err(|e| DbError::InternalError(format!("Lua error: {}", e)))?;
if let LuaValue::UserData(ref ud) = lua_result {
if let Ok(raw) = ud.borrow::<crate::scripting::conversion::RawJson>() {
return Ok(ScriptResult {
status: 200,
body: JsonValue::Null,
headers: HashMap::new(),
raw_body: Some(raw.0.clone()),
});
}
}
let body = self.lua_to_json(lua, lua_result)?;
Ok(ScriptResult {
status: 200,
body,
headers: HashMap::new(),
raw_body: None,
})
})
}
async fn execute_without_pool(
&self,
script: &Script,
db_name: &str,
context: &ScriptContext,
) -> Result<ScriptResult, DbError> {
let lua = Lua::new();
LuaPool::apply_memory_limit(&lua);
let globals = lua.globals();
globals
.set("os", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure os: {}", e)))?;
globals
.set("io", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure io: {}", e)))?;
globals
.set("debug", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure debug: {}", e)))?;
globals
.set("package", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure package: {}", e)))?;
globals
.set("dofile", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure dofile: {}", e)))?;
globals
.set("load", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure load: {}", e)))?;
globals
.set("loadfile", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure loadfile: {}", e)))?;
globals
.set("require", LuaValue::Nil)
.map_err(|e| DbError::InternalError(format!("Failed to secure require: {}", e)))?;
self.setup_lua_globals(&lua, db_name, context, Some((&script.key, &script.name)))?;
install_deadline_hook(&lua);
let chunk = lua.load(&script.code);
let eval_result = chunk.eval_async::<LuaValue>().await;
lua.remove_hook();
match eval_result {
Ok(result) => {
let json_result = self.lua_to_json(&lua, result)?;
Ok(ScriptResult {
status: 200,
body: json_result,
headers: HashMap::new(),
raw_body: None,
})
}
Err(e) => Err(DbError::InternalError(format!("Lua error: {}", e))),
}
}
pub async fn execute_ws(
&self,
script: &Script,
db_name: &str,
context: &ScriptContext,
ws: axum::extract::ws::WebSocket,
) -> Result<(), DbError> {
if !lua_runtime_enabled() {
return Err(lua_disabled_error());
}
websocket::execute_ws(self, script, db_name, context, ws).await
}
pub async fn execute_repl(
&self,
code: &str,
db_name: &str,
variables: &HashMap<String, JsonValue>,
history: &[String],
output_capture: &mut Vec<String>,
) -> Result<(JsonValue, HashMap<String, JsonValue>), DbError> {
if !lua_runtime_enabled() {
return Err(lua_disabled_error());
}
repl::execute_repl(self, code, db_name, variables, history, output_capture).await
}
pub(crate) fn setup_lua_globals(
&self,
lua: &Lua,
db_name: &str,
context: &ScriptContext,
script_info: Option<(&str, &str)>,
) -> Result<(), DbError> {
globals::setup_lua_globals(self, lua, db_name, context, script_info)
}
pub(crate) fn lua_to_json(&self, lua: &Lua, value: LuaValue) -> Result<JsonValue, DbError> {
lua_to_json_value(lua, value)
.map_err(|e| DbError::InternalError(format!("Failed to convert Lua to JSON: {}", e)))
}
}
#[cfg(test)]
mod lua_off_tests {
use super::*;
#[test]
fn env_flag_accepts_common_truthy_values() {
assert!(env_flag_is_set(Some("1")));
assert!(env_flag_is_set(Some("true")));
assert!(env_flag_is_set(Some("YES")));
assert!(!env_flag_is_set(Some("0")));
assert!(!env_flag_is_set(Some("false")));
assert!(!env_flag_is_set(None));
assert!(!env_flag_is_set(Some("")));
}
}