use std::sync::Arc; use std::task::Poll; use futures_util::future::{AbortHandle, Abortable}; use litellm_host::event::{FailureOrigin, Timing, epoch_seconds}; use litellm_host::host::{Demand, HostOp, HostResult, HostStep}; use litellm_host::machine::{HostFailure, Machine, MachineStep}; use litellm_host::route::Route; use pyo3::exceptions::{PyBaseException, PyException, PyRuntimeError}; use pyo3::gc::{PyTraverseError, PyVisit}; use pyo3::prelude::*; use pyo3::types::PyDict; use tokio::sync::Mutex; use crate::adapter::{ InvokeError, LifecycleEvent, LifecycleStep, PythonLifecycle, RouteHost, missing_state, }; use crate::execution::{poll_async_value, run_async_value, run_sync_value}; use crate::handle::{Execution, ExecutionBody, ExecutionStep}; type RouteOf = ::Route; type ErrorOf = as Route>::Error; type ResponseOf = as Route>::Response; type NativeStep = MachineStep, ResponseOf>; type NativeResult = Result, ErrorOf>; type NativeResume = Option>, HostFailure>>>; type MachineResult = Result< MachineStep<::Route, ::Complete>, <::Route as Route>::Error, >; struct MachineState { machine: M, result: Option>, } enum Stage { Begin, Call, Streaming, AfterSuccess, Succeeded(Py), Failed(Py), } #[derive(Clone, Copy)] enum Expect { Started, Arguments, Wire, Emitted, Response, Terminal, } enum Pending { Native, Adapter(Expect), /// The stream handed to the caller waits for its next read or its close. Consumer, } enum Next { Return(ExecutionStep), Continue(HostStep, Py>), } struct PythonDriver where H: RouteHost, M: Machine> + 'static, { route: H, adapter: Box, machine: Option>>>, arguments: Option>, started_at: f64, ended_at: Option, stage: Stage, pending: Option, native_abort: Option, interrupted: Option>, asynchronous: bool, } /// Runs one native call for Python: synchronously, or as a coroutine that awaits every /// host suspension inline in the caller's task. pub fn run_call( py: Python<'_>, machine: M, route: H, adapter: Box, arguments: Py, asynchronous: bool, ) -> PyResult> where H: RouteHost + 'static, M: Machine> + 'static, { let mut driver = PythonDriver { route, adapter, machine: Some(Arc::new(Mutex::new(MachineState { machine, result: None, }))), arguments: Some(arguments), started_at: 0.0, ended_at: None, stage: Stage::Begin, pending: None, native_abort: None, interrupted: None, asynchronous, }; if asynchronous { let execution = Py::new(py, Execution::new(driver))?; return py .import("litellm.rust_bridge.lifecycle")? .getattr("drive")? .call1((execution,)) .map(Bound::unbind); } match driver.resume(None)? { ExecutionStep::Return(value) => Ok(value), ExecutionStep::Open => py .import("litellm.rust_bridge.lifecycle")? .getattr("SyncStream")? .call1((Py::new(py, Execution::suspended(driver))?,)) .map(Bound::unbind), ExecutionStep::Await(_) | ExecutionStep::Yield(_) => { Err(PyRuntimeError::new_err("sync call suspended")) } } } fn is_cancellation(py: Python<'_>, error: &PyErr) -> bool { !error.is_instance_of::(py) } impl PythonDriver where H: RouteHost, M: Machine> + 'static, { fn timing(&self) -> Timing { Timing { start_time: self.started_at, end_time: self.ended_at.unwrap_or_else(epoch_seconds), } } fn drive( &mut self, py: Python<'_>, result: Option>>, ) -> PyResult { match (self.pending.take(), result) { (None, None) => { self.started_at = epoch_seconds(); let started = LifecycleEvent::Started { start_time: self.started_at, }; match self.adapter.emit(py, started) { Ok(step) => self.on_adapter(py, step, Expect::Started), Err(error) => self.adapter_failed(py, error), } } (Some(Pending::Native), Some(Ok(_))) => { let result = self.take_native_result()?; self.run_steps(py, HostStep::Ready(result)) } (Some(Pending::Native), Some(Err(error))) => self.interrupt(py, error), (Some(Pending::Consumer), Some(read)) => { let demand = if read.is_ok() { Demand::More } else { Demand::Detached }; self.resume_machine(py, Some(Ok(HostResult::Demand(demand)))) } (Some(Pending::Adapter(expect)), Some(result)) => { match self.adapter.resume(py, result) { Ok(step) => self.on_adapter(py, step, expect), Err(error) => self.adapter_failed(py, error), } } _ => Err(missing_state()), } } fn on_adapter( &mut self, py: Python<'_>, step: LifecycleStep, expect: Expect, ) -> PyResult { match (expect, step) { (_, LifecycleStep::Await(awaitable)) => { self.pending = Some(Pending::Adapter(expect)); Ok(ExecutionStep::Await(awaitable)) } (Expect::Started, LifecycleStep::Done) => self.begin(py), (Expect::Arguments, LifecycleStep::Arguments(arguments)) => { self.arguments = Some(arguments); self.stage = Stage::Call; self.resume_machine(py, None) } (Expect::Wire, LifecycleStep::Wire(wire)) => { self.resume_machine(py, Some(Ok(HostResult::BeforeSend(wire)))) } (Expect::Emitted, LifecycleStep::Done) => { self.resume_machine(py, Some(Ok(HostResult::Emitted))) } (Expect::Response, LifecycleStep::Response(response)) => self.succeeded(py, response), (Expect::Terminal, LifecycleStep::Done) => match &self.stage { Stage::Succeeded(response) => Ok(ExecutionStep::Return(response.clone_ref(py))), Stage::Failed(error) => Err(PyErr::from_value(error.bind(py).clone().into_any())), _ => Err(missing_state()), }, _ => Err(missing_state()), } } fn begin(&mut self, py: Python<'_>) -> PyResult { let arguments = self.arguments.take().ok_or_else(missing_state)?; match self.adapter.begin(py, arguments, self.started_at) { Ok(step) => self.on_adapter(py, step, Expect::Arguments), Err(error) => self.adapter_failed(py, error), } } fn adapter_failed(&mut self, py: Python<'_>, error: PyErr) -> PyResult { match self.stage { Stage::Begin | Stage::AfterSuccess => self.failure(py, error, FailureOrigin::Host), Stage::Call | Stage::Streaming => self.interrupt(py, error), Stage::Succeeded(_) | Stage::Failed(_) => Err(error), } } fn resume_machine( &mut self, py: Python<'_>, result: NativeResume, ) -> PyResult { let step = self.resume_core(py, result)?; self.run_steps(py, step) } fn run_steps( &mut self, py: Python<'_>, mut step: HostStep, Py>, ) -> PyResult { loop { let result = match step { HostStep::Suspend(awaitable) => { self.pending = Some(Pending::Native); return Ok(ExecutionStep::Await(awaitable)); } HostStep::Ready(result) => result, }; step = match self.handle_native(py, result)? { Next::Return(step) => return Ok(step), Next::Continue(step) => step, }; } } /// Answers one machine step: performs the op it asked for, or finishes the call. fn handle_native(&mut self, py: Python<'_>, result: NativeResult) -> PyResult> { let op = match result { Ok(MachineStep::Host(op)) => op, Ok(MachineStep::Complete(response)) => { return self.completed(py, response).map(Next::Return); } Err(error) => return self.machine_failed(py, error).map(Next::Return), }; let answer = match op { HostOp::Route(op) => { let arguments = self.arguments.as_ref().ok_or_else(missing_state)?; match self.route.invoke(py, arguments.bind(py), op) { Ok(result) => Ok(HostResult::Route(result)), Err(InvokeError::Native(error)) => { return self .resume_core(py, Some(Err(HostFailure::Error(error)))) .map(Next::Continue); } Err(InvokeError::Python(error)) => Err(error), } } HostOp::BeforeSend { wire, context } => { match self.adapter.before_send(py, wire, &context) { Ok(LifecycleStep::Wire(wire)) => Ok(HostResult::BeforeSend(wire)), Ok(LifecycleStep::Await(awaitable)) => { self.pending = Some(Pending::Adapter(Expect::Wire)); return Ok(Next::Return(ExecutionStep::Await(awaitable))); } Ok(_) => return Err(missing_state()), Err(error) => Err(error), } } HostOp::Open(_) => return self.opened(py).map(Next::Return), HostOp::Deliver(chunk) => return self.delivered(py, chunk).map(Next::Return), HostOp::Emit(event) => match self.adapter.emit(py, LifecycleEvent::Machine(&event)) { Ok(LifecycleStep::Done) => Ok(HostResult::Emitted), Ok(LifecycleStep::Await(awaitable)) => { self.pending = Some(Pending::Adapter(Expect::Emitted)); return Ok(Next::Return(ExecutionStep::Await(awaitable))); } Ok(_) => return Err(missing_state()), Err(error) => Err(error), }, }; match answer { Ok(answer) => self.resume_core(py, Some(Ok(answer))).map(Next::Continue), Err(error) => self.interrupt(py, error).map(Next::Return), } } fn opened(&mut self, py: Python<'_>) -> PyResult { self.stage = Stage::Streaming; match self.adapter.opened(py) { Ok(()) => { self.pending = Some(Pending::Consumer); Ok(ExecutionStep::Open) } Err(error) => self.interrupt(py, error), } } fn delivered( &mut self, py: Python<'_>, chunk: as Route>::Chunk, ) -> PyResult { let chunk = match self.route.chunk(py, chunk) { Ok(chunk) => chunk, Err(error) => return self.interrupt(py, error), }; match self.adapter.delivered(py, &chunk) { Ok(()) => { self.pending = Some(Pending::Consumer); Ok(ExecutionStep::Yield(chunk)) } Err(error) => self.interrupt(py, error), } } fn interrupt(&mut self, py: Python<'_>, error: PyErr) -> PyResult { let cancelled = is_cancellation(py, &error); let native = H::host_error(&error); self.interrupted = Some(error.into_value(py)); let failure = if cancelled { HostFailure::Cancelled(native) } else { HostFailure::Error(native) }; self.resume_machine(py, Some(Err(failure))) } fn resume_core( &mut self, py: Python<'_>, result: NativeResume, ) -> PyResult, Py>> { let state = Arc::clone(self.machine.as_ref().ok_or_else(missing_state)?); let future = async move { let mut state = state.lock().await; let result = match result { Some(Err(failure)) => state .machine .interrupt(failure) .await .map(MachineStep::Complete), Some(Ok(result)) => state.machine.resume(Some(result)).await, None => state.machine.resume(None).await, }; state.result = Some(result); Ok(()) }; if self.asynchronous { let mut future = Box::pin(future); if let Poll::Ready(()) = poll_async_value(py, future.as_mut())? { return Ok(HostStep::Ready(self.take_native_result()?)); } let (abort, registration) = AbortHandle::new_pair(); self.native_abort = Some(abort); Ok(HostStep::Suspend( run_async_value(py, async move { Abortable::new(future, registration) .await .map_err(|_| PyRuntimeError::new_err("native execution closed"))? })? .unbind(), )) } else { run_sync_value(py, future)?; Ok(HostStep::Ready(self.take_native_result()?)) } } fn take_native_result(&self) -> PyResult> { self.machine .as_ref() .ok_or_else(missing_state)? .try_lock() .map_err(|_| missing_state())? .result .take() .ok_or_else(missing_state) } fn completed(&mut self, py: Python<'_>, response: ResponseOf) -> PyResult { self.ended_at = Some(epoch_seconds()); let public = match self.route.complete(py, response) { Ok(public) => public, Err(error) => return self.failure(py, error, FailureOrigin::Call), }; if let Stage::Streaming = self.stage { return self.succeeded(py, public); } self.stage = Stage::AfterSuccess; match self.adapter.after_success(py, public, self.timing()) { Ok(step) => self.on_adapter(py, step, Expect::Response), Err(error) => self.failure(py, error, FailureOrigin::Host), } } fn machine_failed(&mut self, py: Python<'_>, error: ErrorOf) -> PyResult { self.ended_at.get_or_insert_with(epoch_seconds); let error = match self.interrupted.take() { Some(retained) => PyErr::from_value(retained.into_bound(py).into_any()), None => self.classified(py, error), }; self.failure(py, error, FailureOrigin::Call) } /// The route's public exception for a native failure. When classification itself /// fails, that failure is raised with the native error's text as its `__context__`. fn classified(&self, py: Python<'_>, error: ErrorOf) -> PyErr { let native = error.to_string(); let classifier_error = match self.route.classify(py, error) { Ok(failure) => return failure.into(), Err(classifier_error) => classifier_error, }; classifier_error.set_context(py, Some(PyRuntimeError::new_err(native))); classifier_error } fn succeeded(&mut self, py: Python<'_>, response: Py) -> PyResult { let event = LifecycleEvent::Succeeded { timing: self.timing(), response: &response, }; let step = self.adapter.emit(py, event)?; self.stage = Stage::Succeeded(response); self.on_adapter(py, step, Expect::Terminal) } fn failure( &mut self, py: Python<'_>, error: PyErr, origin: FailureOrigin, ) -> PyResult { self.ended_at.get_or_insert_with(epoch_seconds); if is_cancellation(py, &error) { return Err(error); } let event = LifecycleEvent::Failed { timing: self.timing(), origin, error: &error, }; let step = self.adapter.emit(py, event)?; self.stage = Stage::Failed(error.into_value(py)); self.on_adapter(py, step, Expect::Terminal) } fn clear(&mut self) { if let Some(abort) = self.native_abort.take() { abort.abort(); } if self.machine.take().is_some() { Python::attach(|py| { self.adapter.close(py); self.route.close(py); }); } } } impl ExecutionBody for PythonDriver where H: RouteHost, M: Machine> + 'static, { fn resume(&mut self, result: Option>>) -> PyResult { Python::attach(|py| self.drive(py, result)) } fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { self.route.traverse(visit)?; self.adapter.traverse(visit)?; visit.call(&self.arguments)?; visit.call(&self.interrupted)?; match &self.stage { Stage::Succeeded(response) => visit.call(response), Stage::Failed(error) => visit.call(error), _ => Ok(()), } } } impl Drop for PythonDriver where H: RouteHost, M: Machine> + 'static, { fn drop(&mut self) { self.clear(); } } #[cfg(test)] mod tests { use std::sync::{Arc, Mutex}; use litellm_host::event::{MachineEvent, RequestContext, WireRequest}; use litellm_host::machine::{Interrupted, Step}; use pyo3::exceptions::{PyBaseException, PyValueError}; use pyo3::types::PyDict; use super::*; static PYTHON_GLOBALS: Mutex<()> = Mutex::new(()); fn install_lifecycle_module(py: Python<'_>) -> Bound<'_, PyModule> { py.run( pyo3::ffi::c_str!( r#" import sys import types sys.modules.setdefault('litellm', types.ModuleType('litellm')) sys.modules.setdefault('litellm.rust_bridge', types.ModuleType('litellm.rust_bridge')) "# ), None, None, ) .unwrap(); let source = std::ffi::CString::new(include_str!("../../../../litellm/rust_bridge/lifecycle.py")) .unwrap(); PyModule::from_code( py, &source, pyo3::ffi::c_str!("lifecycle.py"), pyo3::ffi::c_str!("litellm.rust_bridge.lifecycle"), ) .unwrap() } #[derive(Clone, Debug, PartialEq, Eq)] struct Error(String); impl std::fmt::Display for Error { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str(&self.0) } } struct Synthetic; impl Route for Synthetic { type Response = String; type Error = Error; type Op = &'static str; type OpResult = String; type Chunk = std::convert::Infallible; type StreamHead = std::convert::Infallible; } /// Yields the scripted ops in order, then completes or fails as scripted. struct ScriptedMachine { ops: Vec>, outcome: Option>, answers: Vec, } fn wire() -> WireRequest { WireRequest { url: "https://example.invalid".into(), headers: Vec::new(), body: serde_json::json!({}), } } fn context() -> RequestContext { RequestContext { model: "model".into(), custom_llm_provider: "provider".into(), optional_params: serde_json::json!({}), secret_fields: Vec::new(), api_key: None, } } impl Machine for ScriptedMachine { type Route = Synthetic; type Complete = String; fn resume(&mut self, result: Option>) -> Step<'_, Self> { Box::pin(async move { if let Some(result) = result { self.answers.push(match result { HostResult::Route(value) => value, HostResult::BeforeSend(wire) => wire.url, HostResult::Emitted => "emitted".into(), HostResult::Demand(demand) => format!("{demand:?}"), }); } if !self.ops.is_empty() { return Ok(MachineStep::Host(self.ops.remove(0))); } self.outcome .take() .ok_or_else(|| Error("resumed after completion".into()))? .map(MachineStep::Complete) }) } fn interrupt(&mut self, failure: HostFailure) -> Interrupted<'_, Self> { self.ops.clear(); self.outcome = None; Box::pin(async move { Err(failure.into_error()) }) } } #[derive(Default)] struct Log(Arc>>); impl Log { fn push(&self, entry: impl Into) { self.0.lock().unwrap().push(entry.into()); } fn entries(&self) -> Vec { self.0.lock().unwrap().clone() } } #[derive(Clone, Copy)] enum OpScript { Answer, RaisePython, RejectNatively, } struct SyntheticHost { log: Log, op: OpScript, classifier_fails: bool, } /// The fake route's public exception, kept as a value so a test sees what `classify` /// produced before the driver raises it. #[derive(Debug, PartialEq, Eq)] struct Classified(String); impl From for PyErr { fn from(classified: Classified) -> Self { PyValueError::new_err(format!("classified: {}", classified.0)) } } impl RouteHost for SyntheticHost { type Route = Synthetic; type Failure = Classified; fn invoke( &mut self, _: Python<'_>, arguments: &Bound<'_, PyDict>, op: &'static str, ) -> Result> { self.log.push(format!("route:{op}")); match self.op { OpScript::Answer => Ok(format!("{op}:{}", arguments.len())), OpScript::RaisePython => Err(PyValueError::new_err("op failed").into()), OpScript::RejectNatively => Err(InvokeError::Native(Error("op rejected".into()))), } } fn chunk(&mut self, _: Python<'_>, chunk: std::convert::Infallible) -> PyResult> { match chunk {} } fn complete(&mut self, py: Python<'_>, response: String) -> PyResult> { self.log.push("complete"); Ok(pyo3::types::PyString::new(py, &response) .into_any() .unbind()) } fn classify(&self, _: Python<'_>, error: Error) -> PyResult { self.log.push(format!("classify:{error}")); if self.classifier_fails { return Err(pyo3::exceptions::PyTypeError::new_err("classifier failed")); } Ok(Classified(error.0)) } fn host_error(error: &PyErr) -> Error { Error(error.to_string()) } fn close(&mut self, _: Python<'_>) { self.log.push("route.close"); } fn traverse(&self, _: &PyVisit<'_>) -> Result<(), PyTraverseError> { Ok(()) } } #[derive(Clone, Copy)] enum AdapterScript { Plain, FailBegin, ReplaceResponse, FailAfterSuccess, } struct SyntheticAdapter { log: Log, script: AdapterScript, } impl PythonLifecycle for SyntheticAdapter { fn begin( &mut self, _: Python<'_>, arguments: Py, _: f64, ) -> PyResult { self.log.push("begin"); if matches!(self.script, AdapterScript::FailBegin) { return Err(PyValueError::new_err("begin failed")); } Ok(LifecycleStep::Arguments(arguments)) } fn before_send( &mut self, _: Python<'_>, wire: Box, _: &RequestContext, ) -> PyResult { self.log.push("before_send"); Ok(LifecycleStep::Wire(Box::new(WireRequest { url: "rewritten".into(), ..*wire }))) } fn after_success( &mut self, py: Python<'_>, response: Py, _: Timing, ) -> PyResult { self.log.push("after_success"); match self.script { AdapterScript::ReplaceResponse => Ok(LifecycleStep::Response( "replaced".into_pyobject(py)?.into_any().unbind(), )), AdapterScript::FailAfterSuccess => { Err(PyValueError::new_err("after_success failed")) } AdapterScript::Plain | AdapterScript::FailBegin => { Ok(LifecycleStep::Response(response)) } } } fn emit(&mut self, py: Python<'_>, event: LifecycleEvent<'_>) -> PyResult { self.log.push(match event { LifecycleEvent::Started { .. } => "started".into(), LifecycleEvent::Machine(MachineEvent::ResponseReceived { raw }) => { format!("response:{}", raw.body) } LifecycleEvent::Succeeded { response, .. } => { format!("succeeded:{}", response.bind(py)) } LifecycleEvent::Failed { origin, error, .. } => { format!("failed:{origin:?}:{}", error.value(py)) } }); Ok(LifecycleStep::Done) } fn opened(&mut self, _: Python<'_>) -> PyResult<()> { self.log.push("opened"); Ok(()) } fn delivered(&mut self, _: Python<'_>, _: &Py) -> PyResult<()> { self.log.push("delivered"); Ok(()) } fn resume(&mut self, _: Python<'_>, _: PyResult>) -> PyResult { Err(missing_state()) } fn close(&mut self, _: Python<'_>) { self.log.push("adapter.close"); } fn traverse(&self, _: &PyVisit<'_>) -> Result<(), PyTraverseError> { Ok(()) } } fn run_scripted( py: Python<'_>, machine: ScriptedMachine, op: OpScript, script: AdapterScript, asynchronous: bool, ) -> (PyResult>, Vec) { run_hosted( py, machine, SyntheticHost { log: Log::default(), op, classifier_fails: false, }, script, asynchronous, ) } fn run_hosted( py: Python<'_>, machine: ScriptedMachine, route: SyntheticHost, script: AdapterScript, asynchronous: bool, ) -> (PyResult>, Vec) { let log = Log(route.log.0.clone()); let adapter = SyntheticAdapter { log: Log(log.0.clone()), script, }; let arguments = PyDict::new(py); arguments.set_item("model", "m").unwrap(); let result = run_call( py, machine, route, Box::new(adapter), arguments.unbind(), asynchronous, ); let result = if asynchronous { result.and_then(|coroutine| { let completed = coroutine .call_method1(py, "send", (py.None(),)) .unwrap_err(); if !completed.is_instance_of::(py) { return Err(completed); } completed.value(py).getattr("value").map(Bound::unbind) }) } else { result }; (result, log.entries()) } fn success_machine() -> ScriptedMachine { ScriptedMachine { ops: vec![ HostOp::Route("project"), HostOp::BeforeSend { wire: Box::new(wire()), context: Box::new(context()), }, HostOp::Emit(MachineEvent::ResponseReceived { raw: litellm_host::event::RawResponse { body: "raw".into() }, }), ], outcome: Some(Ok("done".into())), answers: Vec::new(), } } #[test] fn success_runs_every_step_in_order_and_returns_the_public_response() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { install_lifecycle_module(py); for asynchronous in [false, true] { let (result, log) = run_scripted( py, success_machine(), OpScript::Answer, AdapterScript::Plain, asynchronous, ); assert_eq!(result.unwrap().extract::(py).unwrap(), "done"); assert_eq!( log, [ "started", "begin", "route:project", "before_send", "response:raw", "complete", "after_success", "succeeded:done", "adapter.close", "route.close", ] ); } }); } fn failing_machine() -> ScriptedMachine { ScriptedMachine { ops: vec![HostOp::Route("project")], outcome: Some(Err(Error("provider exploded".into()))), answers: Vec::new(), } } #[test] fn a_native_failure_is_classified_once_and_reported_classified() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { install_lifecycle_module(py); for asynchronous in [false, true] { let (result, log) = run_scripted( py, failing_machine(), OpScript::Answer, AdapterScript::Plain, asynchronous, ); let error = result.unwrap_err(); assert!(error.is_instance_of::(py)); assert_eq!(error.value(py).to_string(), "classified: provider exploded"); assert_eq!( log, [ "started", "begin", "route:project", "classify:provider exploded", "failed:Call:classified: provider exploded", "adapter.close", "route.close", ] ); } }); } #[test] fn a_native_rejection_from_a_host_operation_is_classified_once() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { let (result, log) = run_scripted( py, success_machine(), OpScript::RejectNatively, AdapterScript::Plain, false, ); assert_eq!( result.unwrap_err().value(py).to_string(), "classified: op rejected" ); assert_eq!( log, [ "started", "begin", "route:project", "classify:op rejected", "failed:Call:classified: op rejected", "adapter.close", "route.close", ] ); }); } #[test] fn a_python_exception_from_a_host_operation_is_reported_as_raised() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { let (result, log) = run_scripted( py, success_machine(), OpScript::RaisePython, AdapterScript::Plain, false, ); let error = result.unwrap_err(); assert!(error.is_instance_of::(py)); assert_eq!(error.value(py).to_string(), "op failed"); assert_eq!( log, [ "started", "begin", "route:project", "failed:Call:op failed", "adapter.close", "route.close", ] ); }); } #[test] fn a_failing_classifier_surfaces_with_the_native_error_as_context() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { let (result, log) = run_hosted( py, failing_machine(), SyntheticHost { log: Log::default(), op: OpScript::Answer, classifier_fails: true, }, AdapterScript::Plain, false, ); let error = result.unwrap_err(); assert!(error.is_instance_of::(py)); assert_eq!(error.value(py).to_string(), "classifier failed"); let context = error.context(py).unwrap(); assert!(context.is_instance_of::(py)); assert_eq!(context.value(py).to_string(), "provider exploded"); assert_eq!( log, [ "started", "begin", "route:project", "classify:provider exploded", "failed:Call:classifier failed", "adapter.close", "route.close", ] ); }); } #[test] fn begin_failures_are_host_failures_without_provider_mapping() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { let (result, log) = run_scripted( py, success_machine(), OpScript::Answer, AdapterScript::FailBegin, false, ); let error = result.unwrap_err(); assert_eq!(error.value(py).to_string(), "begin failed"); assert_eq!( log, [ "started", "begin", "failed:Host:begin failed", "adapter.close", "route.close" ] ); }); } #[test] fn the_adapters_finalized_response_is_what_the_call_returns_and_reports() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { install_lifecycle_module(py); for asynchronous in [false, true] { let (result, log) = run_scripted( py, success_machine(), OpScript::Answer, AdapterScript::ReplaceResponse, asynchronous, ); assert_eq!(result.unwrap().extract::(py).unwrap(), "replaced"); assert!(log.contains(&"succeeded:replaced".to_string())); assert!(!log.contains(&"succeeded:done".to_string())); } }); } #[test] fn a_failure_while_finalizing_fails_the_call_instead_of_succeeding() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { install_lifecycle_module(py); for asynchronous in [false, true] { let (result, log) = run_scripted( py, success_machine(), OpScript::Answer, AdapterScript::FailAfterSuccess, asynchronous, ); let error = result.unwrap_err(); assert_eq!(error.value(py).to_string(), "after_success failed"); assert_eq!( &log[log.len() - 4..], [ "after_success", "failed:Host:after_success failed", "adapter.close", "route.close" ] ); assert!(!log.iter().any(|entry| entry.starts_with("succeeded"))); } }); } #[test] fn cancellation_ends_the_call_without_terminal_dispatch() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { struct Cancelling(Log); impl RouteHost for Cancelling { type Route = Synthetic; type Failure = Classified; fn invoke( &mut self, _: Python<'_>, _: &Bound<'_, PyDict>, _: &'static str, ) -> Result> { self.0.push("route"); Err(pyo3::exceptions::asyncio::CancelledError::new_err(()).into()) } fn chunk( &mut self, _: Python<'_>, chunk: std::convert::Infallible, ) -> PyResult> { match chunk {} } fn complete(&mut self, _: Python<'_>, _: String) -> PyResult> { Err(missing_state()) } fn classify(&self, _: Python<'_>, error: Error) -> PyResult { self.0.push("classify"); Ok(Classified(error.0)) } fn host_error(error: &PyErr) -> Error { Error(error.to_string()) } fn close(&mut self, _: Python<'_>) {} fn traverse(&self, _: &PyVisit<'_>) -> Result<(), PyTraverseError> { Ok(()) } } let log = Log::default(); let route = Cancelling(Log(log.0.clone())); let adapter = SyntheticAdapter { log: Log(log.0.clone()), script: AdapterScript::Plain, }; let error = run_call( py, success_machine(), route, Box::new(adapter), PyDict::new(py).unbind(), false, ) .unwrap_err(); assert!(!error.is_instance_of::(py)); assert_eq!( log.entries(), ["started", "begin", "route", "adapter.close"] ); }); } #[test] fn python_driver_preserves_inline_await_and_native_ownership() { let _guard = PYTHON_GLOBALS .lock() .unwrap_or_else(|error| error.into_inner()); crate::initialize_python(); Python::attach(|py| { py.import("asyncio").unwrap(); let module = install_lifecycle_module(py); let locals = PyDict::new(py); locals .set_item("drive", module.getattr("drive").unwrap()) .unwrap(); locals .set_item( "await_execution", wrap_pyfunction!(await_execution, py).unwrap(), ) .unwrap(); locals .set_item( "calling_execution", wrap_pyfunction!(calling_execution, py).unwrap(), ) .unwrap(); let probe = std::ffi::CString::new(include_str!("../tests/lifecycle.py")).unwrap(); py.run(&probe, Some(&locals), Some(&locals)).unwrap(); }); } struct RetainingHost { retained: Option>, } impl ExecutionBody for RetainingHost { fn resume(&mut self, _: Option>>) -> PyResult { Python::attach(|py| Ok(ExecutionStep::Return(py.None()))) } fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { visit.call(&self.retained) } } #[pyfunction] fn retaining_coroutine(py: Python<'_>, retained: Py) -> PyResult> { Py::new( py, Execution::new(RetainingHost { retained: Some(retained), }), ) } struct AwaitBody(Option>); impl ExecutionBody for AwaitBody { fn resume(&mut self, result: Option>>) -> PyResult { match self.0.take() { Some(awaitable) => Ok(ExecutionStep::Await(awaitable)), None => result .expect("selected await completed") .map(ExecutionStep::Return), } } fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { visit.call(&self.0) } } #[pyfunction] fn await_execution(awaitable: Py) -> Execution { Execution::new(AwaitBody(Some(awaitable))) } struct CallingBody(Py); impl ExecutionBody for CallingBody { fn resume(&mut self, _: Option>>) -> PyResult { Python::attach(|py| self.0.call0(py).map(ExecutionStep::Return)) } fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { visit.call(&self.0) } } #[pyfunction] fn calling_execution(callback: Py) -> Execution { Execution::new(CallingBody(callback)) } struct ErrorBody(Option>); impl ExecutionBody for ErrorBody { fn resume(&mut self, _: Option>>) -> PyResult { Python::attach(|py| { Err(PyErr::from_value( self.0.take().unwrap().into_bound(py).into_any(), )) }) } fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { visit.call(&self.0) } } #[pyfunction] fn error_execution(error: Bound<'_, PyBaseException>) -> Execution { Execution::new(ErrorBody(Some(error.unbind()))) } #[test] fn retained_exception_frames_are_collectable() { crate::initialize_python(); Python::attach(|py| { let locals = PyDict::new(py); locals .set_item( "error_execution", wrap_pyfunction!(error_execution, py).unwrap(), ) .unwrap(); py.run( pyo3::ffi::c_str!( r#" import gc import weakref class Retained: pass def cycle(): retained = Retained() try: raise ValueError('retained traceback') except ValueError as error: retained.owner = error_execution(error) return weakref.ref(retained) reference = cycle() gc.collect() assert reference() is None "# ), Some(&locals), Some(&locals), ) .unwrap(); }); } #[test] fn coroutine_collects_cycles_retained_by_bridge_host() { crate::initialize_python(); Python::attach(|py| { let locals = PyDict::new(py); locals .set_item( "retaining_coroutine", wrap_pyfunction!(retaining_coroutine, py).unwrap(), ) .unwrap(); py.run( pyo3::ffi::c_str!( r#" import gc import weakref class Retained: pass def cycle(): retained = Retained() coroutine = retaining_coroutine(retained) retained.coroutine = coroutine return weakref.ref(retained) retained_ref = cycle() gc.collect() assert retained_ref() is None "# ), Some(&locals), Some(&locals), ) .unwrap(); }); } }