use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use crate::computation_graph::scheduler::ComputationGraphDeclaration;
use crate::runtime::Runtime;
use crate::task::TaskNamespace;
pub struct PythonTaskNode {
pub id: String,
pub dependencies: Vec<String>,
}
pub struct LoadedPythonWorkflow {
pub task_namespaces: Vec<TaskNamespace>,
pub tasks: Vec<PythonTaskNode>,
pub workflow_name: String,
}
pub trait PythonRuntime: Send + Sync {
fn load_workflow_package(
&self,
archive_data: &[u8],
staging_dir: &Path,
tenant_id: &str,
runtime: &Arc<Runtime>,
) -> Result<LoadedPythonWorkflow, String>;
#[allow(clippy::too_many_arguments)]
fn load_cg_package(
&self,
archive_data: &[u8],
staging_dir: &Path,
tenant_id: &str,
graph_name: &str,
entry_module: &str,
accumulator_overrides: &[cloacina_workflow_plugin::types::AccumulatorConfig],
runtime: &Arc<Runtime>,
) -> Result<Option<ComputationGraphDeclaration>, String>;
}
static PYTHON_RUNTIME: OnceLock<Arc<dyn PythonRuntime>> = OnceLock::new();
pub fn register_python_runtime(runtime: Arc<dyn PythonRuntime>) {
let _ = PYTHON_RUNTIME.set(runtime);
init_python_runtime_health_metrics();
}
pub fn python_runtime() -> Option<Arc<dyn PythonRuntime>> {
PYTHON_RUNTIME.get().cloned()
}
static PYTHON_RUNTIME_WEDGED: AtomicBool = AtomicBool::new(false);
static PYTHON_RUNTIME_WEDGED_REASON: Mutex<Option<String>> = Mutex::new(None);
pub fn mark_python_runtime_wedged(reason: impl Into<String>) {
let reason = reason.into();
let first = !PYTHON_RUNTIME_WEDGED.swap(true, Ordering::SeqCst);
if first {
if let Ok(mut slot) = PYTHON_RUNTIME_WEDGED_REASON.lock() {
*slot = Some(reason.clone());
}
}
metrics::gauge!("cloacina_python_runtime_wedged").set(1.0);
tracing::error!(
reason = %reason,
first_wedge = first,
"PYTHON RUNTIME WEDGED — the embedded interpreter is holding the GIL and \
cannot be recovered in-process. All further Python package loads in this \
process will hang or fail; /ready now reports not-ready. Restart the \
process to recover, and remove/fix the offending package first."
);
}
pub fn is_python_runtime_wedged() -> bool {
PYTHON_RUNTIME_WEDGED.load(Ordering::SeqCst)
}
pub fn python_runtime_wedged_reason() -> Option<String> {
if !is_python_runtime_wedged() {
return None;
}
PYTHON_RUNTIME_WEDGED_REASON
.lock()
.ok()
.and_then(|slot| slot.clone())
.or_else(|| Some("python runtime wedged (reason unavailable)".to_string()))
}
pub fn record_python_import_interrupted() {
metrics::counter!("cloacina_python_import_interrupted_total").increment(1);
}
#[doc(hidden)]
pub fn reset_python_runtime_wedged_for_tests() {
PYTHON_RUNTIME_WEDGED.store(false, Ordering::SeqCst);
if let Ok(mut slot) = PYTHON_RUNTIME_WEDGED_REASON.lock() {
*slot = None;
}
metrics::gauge!("cloacina_python_runtime_wedged").set(0.0);
}
pub fn init_python_runtime_health_metrics() {
if !is_python_runtime_wedged() {
metrics::gauge!("cloacina_python_runtime_wedged").set(0.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wedged_flag_latches_first_reason() {
reset_python_runtime_wedged_for_tests();
assert!(!is_python_runtime_wedged());
assert_eq!(python_runtime_wedged_reason(), None);
mark_python_runtime_wedged("python runtime wedged by package alpha import hang");
assert!(is_python_runtime_wedged());
assert_eq!(
python_runtime_wedged_reason().as_deref(),
Some("python runtime wedged by package alpha import hang")
);
mark_python_runtime_wedged("python runtime wedged by package beta import hang");
assert_eq!(
python_runtime_wedged_reason().as_deref(),
Some("python runtime wedged by package alpha import hang")
);
reset_python_runtime_wedged_for_tests();
assert!(!is_python_runtime_wedged());
}
}