#[cfg(feature = "telemetry")]
use std::time::Instant;
use std::{
borrow::Cow,
fmt,
future::{Future, ready},
mem,
path::Path,
pin::Pin,
process::ExitStatus,
sync::Arc,
time::Duration,
};
use monty_fs::{MountCallOutcome, MountMode, MountRoot, MountTable, OverlayState};
use monty_proto::{FrameError, PROTOCOL_VERSION, exceeds_max_value_depth, pb, validate_requirement};
use monty_types::{
AssertMessageAnnotations, DEFAULT_MAX_SUSPENSIONS, ExcType, MONTY_VERSION, MontyException, MontyObject, MontyUuid,
NameLookupResult, OsFunctionCall, PrintStream, ResourceLimits, TypeCheckingConfig,
};
use tokio::{task::spawn_blocking, time::timeout};
#[cfg(feature = "telemetry")]
use crate::telemetry::{TelemetryContext, metrics::outcome};
use crate::{
CrashCause, PoolError,
pool::{CapacityGuard, PoolInner},
worker::Worker,
};
#[derive(Debug, Clone)]
pub struct ReplConfig {
pub script_name: String,
pub limits: Option<ResourceLimits>,
pub type_check: bool,
pub type_check_stubs: Option<String>,
pub type_check_config: TypeCheckingConfig,
pub assert_message_annotations: AssertMessageAnnotations,
pub print_flush_interval: Option<Duration>,
}
impl Default for ReplConfig {
fn default() -> Self {
Self {
script_name: "main.py".to_owned(),
limits: None,
type_check: false,
type_check_stubs: None,
type_check_config: TypeCheckingConfig::default(),
assert_message_annotations: AssertMessageAnnotations::default(),
print_flush_interval: None,
}
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct CheckoutOptions {
#[cfg(feature = "telemetry")]
pub telemetry: Option<TelemetryContext>,
pub connect_headers: Vec<(String, String)>,
}
impl CheckoutOptions {
#[cfg(feature = "telemetry")]
#[must_use]
pub fn with_telemetry(mut self, telemetry: Option<TelemetryContext>) -> Self {
self.telemetry = telemetry;
self
}
#[must_use]
pub fn with_connect_headers(mut self, connect_headers: Vec<(String, String)>) -> Self {
self.connect_headers = connect_headers;
self
}
}
impl fmt::Debug for CheckoutOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let header_names: Vec<&str> = self.connect_headers.iter().map(|(name, _)| name.as_str()).collect();
f.debug_struct("CheckoutOptions")
.field("connect_headers", &header_names)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct MountSpec {
root: MountRoot,
pub mode: MountSpecMode,
pub write_bytes_limit: Option<u64>,
pub memory_usage_limit: u64,
}
impl MountSpec {
pub fn new(virtual_path: &str, host_path: impl AsRef<Path>, mode: MountSpecMode) -> Result<Self, PoolError> {
let root = MountRoot::open(virtual_path, host_path).map_err(|err| PoolError::Runtime(err.into_exception()))?;
Ok(Self::from_root(root, mode))
}
#[must_use]
pub fn from_root(root: MountRoot, mode: MountSpecMode) -> Self {
Self {
root,
mode,
write_bytes_limit: None,
memory_usage_limit: monty_fs::DEFAULT_MEMORY_USAGE_LIMIT,
}
}
#[must_use]
pub fn virtual_path(&self) -> &str {
self.root.virtual_path()
}
#[must_use]
pub fn host_path(&self) -> &Path {
self.root.host_path()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountSpecMode {
ReadOnly,
ReadWrite,
Overlay,
}
#[derive(Debug)]
pub enum TurnEvent {
FunctionCall {
function_name: String,
args: Vec<MontyObject>,
kwargs: Vec<(MontyObject, MontyObject)>,
call_id: u32,
object_id: Option<MontyUuid>,
},
OsCall {
function_name: String,
args: Vec<MontyObject>,
kwargs: Vec<(MontyObject, MontyObject)>,
call_id: u32,
},
NameLookup { name: String, object_id: Option<MontyUuid> },
ResolveFutures { pending_call_ids: Vec<u32> },
Complete(MontyObject),
}
#[derive(Debug)]
pub enum ResumeValue {
Return(MontyObject),
Error(MontyException),
Future,
NotFound,
NotHandled,
}
pub type PrintFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
pub type OnPrint<'a> = &'a mut (dyn FnMut(PrintStream, &str) -> PrintFuture + Send);
pub type OnRawEvent<'a> = &'a mut (dyn FnMut(&pb::ChildEvent) -> PrintFuture + Send);
pub fn on_print_sync<F>(mut sink: F) -> impl FnMut(PrintStream, &str) -> PrintFuture + Send
where
F: FnMut(PrintStream, &str) + Send,
{
move |stream, text| {
sink(stream, text);
Box::pin(ready(()))
}
}
pub struct Checkout {
worker: Option<Worker>,
pool: Arc<PoolInner>,
pending: Option<Pending>,
turn_in_flight: bool,
abort_in_flight: bool,
budget: SessionBudget,
pending_load_budget: Option<SessionBudget>,
armed_deadline: Option<Duration>,
restored_script_name: Option<String>,
feed_mounts: Option<MountTable>,
#[cfg(feature = "telemetry")]
started: Option<Instant>,
}
#[derive(Clone, Copy)]
struct SessionBudget {
duration_budget: Option<Duration>,
reported_execution: Duration,
suspension_limit: u64,
suspensions_seen: u64,
}
impl SessionBudget {
fn from_config(repl: &ReplConfig) -> Self {
let limits = repl.limits.as_ref();
Self {
duration_budget: limits.and_then(|limits| limits.max_duration),
reported_execution: Duration::ZERO,
suspension_limit: limits.map_or(DEFAULT_MAX_SUSPENSIONS as u64, |limits| limits.max_suspensions as u64),
suspensions_seen: 0,
}
}
fn forget(&mut self) {
*self = Self {
duration_budget: None,
reported_execution: Duration::ZERO,
suspension_limit: self.suspension_limit,
suspensions_seen: 0,
};
}
fn update_from(&mut self, event: &pb::ChildEvent) {
self.reported_execution = self
.reported_execution
.max(Duration::from_micros(event.total_execution_micros));
if self.duration_budget.is_none() {
self.duration_budget = event.max_duration_micros.map(Duration::from_micros);
}
if let Some(reported) = event.max_suspensions {
self.suspension_limit = self.suspension_limit.min(reported);
}
if is_suspension(event) {
self.suspensions_seen += 1;
}
}
fn over_suspension_limit(&self, event: &pb::ChildEvent) -> Option<u64> {
(is_suspension(event) && self.suspensions_seen > self.suspension_limit).then_some(self.suspension_limit)
}
fn backstop_deadline(&self, grace: Option<Duration>) -> Option<Duration> {
Some(
self.duration_budget?
.saturating_sub(self.reported_execution)
.saturating_add(grace?),
)
}
}
fn is_suspension(event: &pb::ChildEvent) -> bool {
matches!(
event.kind,
Some(
pb::child_event::Kind::FunctionCall(_)
| pb::child_event::Kind::OsCall(_)
| pb::child_event::Kind::NameLookup(_)
| pb::child_event::Kind::ResolveFutures(_)
)
)
}
fn is_abort_reply(event: &pb::ChildEvent) -> bool {
matches!(
event.kind,
Some(
pb::child_event::Kind::Error(_) | pb::child_event::Kind::FatalError(_) | pb::child_event::Kind::Shutdown(_)
)
)
}
fn suspension_limit_exceeded(limit: u64) -> MontyException {
MontyException::new(
ExcType::RuntimeError,
Some(format!("suspension limit {limit} exceeded")),
)
}
enum Pending {
Call {
call_id: u32,
function_name: String,
os_call: Option<Box<OsFunctionCall>>,
},
NameLookup,
Futures,
}
impl Checkout {
pub(crate) async fn create(worker: Worker, pool: Arc<PoolInner>, repl: &ReplConfig) -> Result<Self, PoolError> {
let request = request(pb::parent_request::Kind::Configure(pb::Configure {
script_name: repl.script_name.clone(),
limits: repl.limits.as_ref().map(Into::into),
type_check: repl.type_check,
type_check_stubs: repl.type_check_stubs.clone(),
type_check_format: pb::TypeCheckFormat::from(repl.type_check_config.format).into(),
type_check_color: repl.type_check_config.color,
assert_message_annotations: Some(repl.assert_message_annotations.max_bytes()),
protocol_version: PROTOCOL_VERSION,
monty_version: MONTY_VERSION.to_owned(),
print_flush_interval_ms: repl.print_flush_interval.map(flush_interval_ms),
}));
let mut this = Self {
worker: Some(worker),
pool,
pending: None,
turn_in_flight: false,
abort_in_flight: false,
budget: SessionBudget::from_config(repl),
pending_load_budget: None,
armed_deadline: None,
restored_script_name: None,
feed_mounts: None,
#[cfg(feature = "telemetry")]
started: Some(Instant::now()),
};
let mut no_print = on_print_sync(|_, _| {});
let deadline = this.pool.config.request_timeout;
match this.request_turn(&request, deadline, &mut no_print).await? {
ControlEvent::Ok => Ok(this),
other => Err(this.protocol_violation(format!("unexpected reply to Configure: {other:?}"))),
}
}
pub async fn restore(
&mut self,
state: Vec<u8>,
mounts: Vec<MountSpec>,
on_print: OnPrint<'_>,
) -> Result<(Option<TurnEvent>, Option<String>), PoolError> {
self.ensure_ready()?;
let feed_mounts = Self::build_feed_mounts(mounts);
self.pending = None;
self.begin_load();
self.restored_script_name = None;
self.feed_mounts = feed_mounts;
let request = request(pb::parent_request::Kind::Load(pb::Load { state }));
let outcome = self
.request_turn(&request, self.pool.config.request_timeout, on_print)
.await;
self.end_load();
let event = match outcome? {
ControlEvent::Ok => None,
ControlEvent::Turn(event) => Some(event),
other @ ControlEvent::Dump(_) => {
return Err(self.protocol_violation(format!("unexpected reply to Load: {other:?}")));
}
};
Ok((event, self.restored_script_name.take()))
}
pub async fn feed(
&mut self,
code: &str,
inputs: Vec<(String, MontyObject)>,
mounts: Vec<MountSpec>,
skip_type_check: bool,
on_print: OnPrint<'_>,
) -> Result<TurnEvent, PoolError> {
self.ensure_ready()?;
if self.pending.is_some() {
return Err(PoolError::Protocol(
"feed called while a suspension is awaiting an answer".into(),
));
}
ensure_sendable(inputs.iter().map(|(_, value)| value))?;
self.feed_mounts = Self::build_feed_mounts(mounts);
let request = request(pb::parent_request::Kind::Feed(pb::Feed {
code: code.to_owned(),
inputs: inputs
.into_iter()
.map(|(name, value)| pb::NamedValue {
name,
value: Some(value.into()),
})
.collect(),
skip_type_check,
}));
self.expect_turn(&request, on_print).await
}
pub async fn resume(&mut self, value: ResumeValue, on_print: OnPrint<'_>) -> Result<TurnEvent, PoolError> {
self.ensure_ready()?;
let Some(Pending::Call {
call_id,
function_name,
os_call,
}) = &self.pending
else {
return Err(PoolError::Protocol("no suspended call to resume".into()));
};
let (call_id, function_name, is_os_call) = (*call_id, function_name.clone(), os_call.is_some());
if matches!(value, ResumeValue::NotHandled) && !is_os_call {
return Err(PoolError::Protocol(
"NotHandled is only valid answering an OS call".into(),
));
}
if let ResumeValue::Return(obj) = &value {
ensure_sendable([obj])?;
}
let result = match value {
ResumeValue::Return(obj) => pb::ext_function_result::Kind::ReturnValue(obj.into()),
ResumeValue::Error(exc) => pb::ext_function_result::Kind::Error((&exc).into()),
ResumeValue::Future => pb::ext_function_result::Kind::Future(call_id),
ResumeValue::NotFound => pb::ext_function_result::Kind::NotFound(function_name),
ResumeValue::NotHandled => pb::ext_function_result::Kind::NotHandled(pb::Unit {}),
};
let request = request(pb::parent_request::Kind::ResumeCall(pb::ResumeCall {
call_id,
result: Some(pb::ExtFunctionResult { kind: Some(result) }),
}));
self.expect_turn(&request, on_print).await
}
pub async fn resume_from_mounts(&mut self, on_print: OnPrint<'_>) -> Result<Option<TurnEvent>, PoolError> {
self.ensure_ready()?;
let Some(Pending::Call { os_call, .. }) = &mut self.pending else {
return Err(PoolError::Protocol("no suspended call to resume".into()));
};
let Some(call) = os_call.take() else {
return Err(PoolError::Protocol(
"resume_from_mounts is only valid answering an OS call".into(),
));
};
let outcome = match self.feed_mounts.take() {
Some(mut mounts) => {
self.turn_in_flight = true;
let (mounts, outcome) = self
.run_blocking(move || {
let outcome = mounts.handle_os_call(*call);
(mounts, outcome)
})
.await?;
self.feed_mounts = Some(mounts);
self.turn_in_flight = false;
outcome
}
None => MountCallOutcome::NotHandled(*call),
};
match outcome {
MountCallOutcome::Handled(result) => {
let value = match result {
Ok(obj) => ResumeValue::Return(obj),
Err(err) => ResumeValue::Error(err.into_exception()),
};
match self.resume(value, &mut *on_print).await {
Err(PoolError::Runtime(exc)) if self.pending.is_some() => {
self.resume(ResumeValue::Error(exc), on_print).await.map(Some)
}
other => other.map(Some),
}
}
MountCallOutcome::NotHandled(call) => {
let Some(Pending::Call { os_call, .. }) = &mut self.pending else {
unreachable!("checked above");
};
*os_call = Some(Box::new(call));
Ok(None)
}
}
}
pub async fn resume_name_lookup(
&mut self,
result: impl Into<NameLookupResult>,
on_print: OnPrint<'_>,
) -> Result<TurnEvent, PoolError> {
self.ensure_ready()?;
if !matches!(self.pending, Some(Pending::NameLookup)) {
return Err(PoolError::Protocol("no suspended name lookup to resume".into()));
}
let kind = match result.into() {
NameLookupResult::Value(obj) => {
ensure_sendable([&obj])?;
pb::resume_name_lookup::Kind::Value(obj.into())
}
NameLookupResult::Undefined => pb::resume_name_lookup::Kind::Undefined(pb::Unit {}),
NameLookupResult::Error(exc) => pb::resume_name_lookup::Kind::Error((&exc).into()),
};
let request = request(pb::parent_request::Kind::ResumeNameLookup(pb::ResumeNameLookup {
kind: Some(kind),
}));
self.expect_turn(&request, on_print).await
}
pub async fn resume_futures(
&mut self,
results: Vec<(u32, ResumeValue)>,
on_print: OnPrint<'_>,
) -> Result<TurnEvent, PoolError> {
self.ensure_ready()?;
if !matches!(self.pending, Some(Pending::Futures)) {
return Err(PoolError::Protocol("no suspended futures to resume".into()));
}
let results = results
.into_iter()
.map(|(call_id, value)| {
if let ResumeValue::Return(obj) = &value {
ensure_sendable([obj])?;
}
let kind = match value {
ResumeValue::Return(obj) => pb::ext_function_result::Kind::ReturnValue(obj.into()),
ResumeValue::Error(exc) => pb::ext_function_result::Kind::Error((&exc).into()),
ResumeValue::Future | ResumeValue::NotFound | ResumeValue::NotHandled => {
return Err(PoolError::Protocol(
format!("future {call_id} must resolve to Return or Error").into(),
));
}
};
Ok(pb::FutureResult {
call_id,
result: Some(pb::ExtFunctionResult { kind: Some(kind) }),
})
})
.collect::<Result<Vec<_>, _>>()?;
let request = request(pb::parent_request::Kind::ResumeFutures(pb::ResumeFutures { results }));
self.expect_turn(&request, on_print).await
}
pub async fn install_dependencies(&mut self, requirements: Vec<String>) -> Result<(), PoolError> {
self.ensure_ready()?;
if self.pending.is_some() {
return Err(PoolError::Protocol(
"install_dependencies called while a suspension is awaiting an answer".into(),
));
}
if requirements.is_empty() {
return Ok(());
}
for requirement in &requirements {
validate_requirement(requirement).map_err(invalid_requirement)?;
}
let request = request(pb::parent_request::Kind::InstallDependencies(pb::InstallDependencies {
requirements,
}));
let mut no_print = on_print_sync(|_, _| {});
let deadline = self.pool.config.request_timeout;
match self.request_turn(&request, deadline, &mut no_print).await? {
ControlEvent::Ok => Ok(()),
other => Err(self.protocol_violation(format!("unexpected reply to InstallDependencies: {other:?}"))),
}
}
pub async fn dump(&mut self) -> Result<Vec<u8>, PoolError> {
let request = request(pb::parent_request::Kind::Dump(pb::Dump {}));
let mut no_print = on_print_sync(|_, _| {});
let deadline = self.pool.config.request_timeout;
match self.request_turn(&request, deadline, &mut no_print).await? {
ControlEvent::Dump(state) => Ok(state),
other => Err(self.protocol_violation(format!("unexpected reply to Dump: {other:?}"))),
}
}
pub async fn finish(mut self) -> Result<(), PoolError> {
let result = self.finish_session().await;
#[cfg(feature = "telemetry")]
self.record_finish(outcome(result.is_ok()));
result
}
async fn finish_session(&mut self) -> Result<(), PoolError> {
if self.pool.config.transport.is_websocket() {
if let Some(mut worker) = self.worker.take() {
let capacity = CapacityGuard::new(&self.pool);
worker.close_transport().await;
capacity.disarm();
self.pool.release_worker(worker);
}
return Ok(());
}
let request = request(pb::parent_request::Kind::Reset(pb::Reset {}));
let mut no_print = on_print_sync(|_, _| {});
let deadline = self.pool.config.request_timeout;
match self.request_turn(&request, deadline, &mut no_print).await? {
ControlEvent::Ok => {
if let Some(mut worker) = self.worker.take() {
worker.checkouts_served += 1;
self.pool.release_worker(worker);
}
Ok(())
}
other => Err(self.protocol_violation(format!("unexpected reply to Reset: {other:?}"))),
}
}
#[cfg(feature = "telemetry")]
fn record_finish(&mut self, outcome: &'static str) {
if let (Some(started), Some(metrics)) = (self.started.take(), &self.pool.config.metrics) {
metrics.session_duration(started.elapsed(), outcome);
}
}
pub fn pid(&self) -> Option<u32> {
self.worker.as_ref().and_then(Worker::pid)
}
async fn expect_turn(
&mut self,
request: &pb::ParentRequest,
on_print: OnPrint<'_>,
) -> Result<TurnEvent, PoolError> {
let deadline = min_deadline(self.pool.config.request_timeout, self.backstop_deadline());
match self.request_turn(request, deadline, on_print).await? {
ControlEvent::Turn(event) => Ok(event),
other => Err(self.protocol_violation(format!("expected a turn event, got {other:?}"))),
}
}
fn backstop_deadline(&self) -> Option<Duration> {
self.budget.backstop_deadline(self.pool.config.duration_limit_grace)
}
fn begin_load(&mut self) {
self.pending_load_budget = Some(self.budget);
self.budget.forget();
}
fn end_load(&mut self) {
if let Some(budget) = self.pending_load_budget.take() {
self.budget = budget;
}
}
async fn abort_if_over_budget(&mut self, event: &pb::ChildEvent) -> Result<bool, PoolError> {
let is_print = matches!(event.kind, Some(pb::child_event::Kind::Print(_)));
if !is_print && mem::take(&mut self.abort_in_flight) && !is_abort_reply(event) {
return Err(self.protocol_violation("worker answered AbortFeed with something other than an Error"));
}
if !is_print
&& let Some(saved) = self.pending_load_budget.take()
&& !(matches!(event.kind, Some(pb::child_event::Kind::Ok(_))) || is_suspension(event))
{
self.budget = saved;
}
self.budget.update_from(event);
let Some(limit) = self.budget.over_suspension_limit(event) else {
return Ok(false);
};
if let Some(pb::child_event::Kind::OsCall(call)) = &event.kind {
match &call.call {
None => return Err(self.protocol_violation("OsCall event with no call")),
Some(kind) => {
if let Err(err) = OsFunctionCall::try_from(kind.clone()) {
return Err(self.protocol_violation(format!("invalid OS call payload: {err}")));
}
}
}
}
let abort = request(pb::parent_request::Kind::AbortFeed(pb::AbortFeed {
exception: Some((&suspension_limit_exceeded(limit)).into()),
}));
let Some(worker) = self.worker.as_mut() else {
return Err(PoolError::Finished);
};
match worker.send(&abort).await {
Ok(()) => {
self.abort_in_flight = true;
Ok(true)
}
Err(_) => Err(self.poison("aborting a feed").await),
}
}
async fn request_turn(
&mut self,
request: &pb::ParentRequest,
deadline: Option<Duration>,
on_print: OnPrint<'_>,
) -> Result<ControlEvent, PoolError> {
self.ensure_ready()?;
self.turn_in_flight = true;
self.armed_deadline = deadline;
let outcome = match deadline {
Some(limit) => match timeout(limit, self.turn_io(request, on_print)).await {
Ok(outcome) => outcome,
Err(_elapsed) => Err(self.poison_timeout().await),
},
None => self.turn_io(request, on_print).await,
};
self.turn_in_flight = false;
outcome
}
pub async fn turn_raw(
&mut self,
request: &pb::ParentRequest,
on_event: OnRawEvent<'_>,
) -> Result<pb::ChildEvent, PoolError> {
self.ensure_ready()?;
if matches!(
request.kind,
None | Some(
pb::parent_request::Kind::Configure(_)
| pb::parent_request::Kind::Reset(_)
| pb::parent_request::Kind::Shutdown(_)
)
) {
return Err(PoolError::Protocol(
"lifecycle requests (Configure/Reset/Shutdown) cannot be driven through turn_raw".into(),
));
}
let is_load = matches!(request.kind, Some(pb::parent_request::Kind::Load(_)));
if is_load {
self.begin_load();
}
self.turn_in_flight = true;
let deadline = min_deadline(self.pool.config.request_timeout, self.backstop_deadline());
self.armed_deadline = deadline;
let outcome = match deadline {
Some(limit) => match timeout(limit, self.turn_io_raw(request, on_event)).await {
Ok(outcome) => outcome,
Err(_elapsed) => Err(self.poison_timeout().await),
},
None => self.turn_io_raw(request, on_event).await,
};
self.turn_in_flight = false;
if is_load {
self.end_load();
}
outcome
}
async fn turn_io_raw(
&mut self,
request: &pb::ParentRequest,
on_event: OnRawEvent<'_>,
) -> Result<pb::ChildEvent, PoolError> {
let Some(worker) = self.worker.as_mut() else {
return Err(PoolError::Finished);
};
if let Err(err) = worker.send(request).await {
return Err(match err {
FrameError::FrameTooLarge { len, max } => PoolError::Runtime(MontyException::new(
ExcType::RuntimeError,
Some(format!(
"request frame of {len} bytes exceeds the maximum of {max} bytes"
)),
)),
_ => self.poison("sending a request").await,
});
}
loop {
let event = match self.worker.as_mut().expect("checked above").recv().await {
Ok(event) => event,
Err(FrameError::Decode(err)) => {
return Err(self.protocol_violation(format!("invalid payload from worker: {err}")));
}
Err(_) => return Err(self.poison("waiting for a reply").await),
};
if self.abort_if_over_budget(&event).await? {
continue;
}
if matches!(event.kind, Some(pb::child_event::Kind::Print(_))) {
on_event(&event).await;
} else if matches!(event.kind, Some(pb::child_event::Kind::FatalError(_))) {
self.reap_announced_exit().await;
return Ok(event);
} else if matches!(event.kind, Some(pb::child_event::Kind::Shutdown(_))) {
return if self.pool.config.transport.is_websocket() {
self.discard_worker();
Ok(event)
} else {
Err(self.protocol_violation("subprocess worker sent a ShutdownDump"))
};
} else if event.kind.is_none() {
return Err(self.protocol_violation("worker sent an event with no kind"));
} else {
return Ok(event);
}
}
}
fn ensure_ready(&mut self) -> Result<(), PoolError> {
if self.worker.is_none() {
Err(PoolError::Finished)
} else if self.turn_in_flight {
self.discard_worker();
Err(PoolError::Protocol(
"a previous protocol turn was cancelled mid-flight; the worker was discarded".into(),
))
} else {
Ok(())
}
}
async fn turn_io(&mut self, request: &pb::ParentRequest, on_print: OnPrint<'_>) -> Result<ControlEvent, PoolError> {
let Some(worker) = self.worker.as_mut() else {
return Err(PoolError::Finished);
};
if let Err(err) = worker.send(request).await {
return Err(match err {
FrameError::FrameTooLarge { len, max } => PoolError::Runtime(MontyException::new(
ExcType::RuntimeError,
Some(format!(
"request frame of {len} bytes exceeds the maximum of {max} bytes"
)),
)),
_ => self.poison("sending a request").await,
});
}
loop {
let event = match self.worker.as_mut().expect("checked above").recv().await {
Ok(event) => event,
Err(FrameError::Decode(err)) => {
return Err(self.protocol_violation(format!("invalid payload from worker: {err}")));
}
Err(_) => return Err(self.poison("waiting for a reply").await),
};
if self.abort_if_over_budget(&event).await? {
continue;
}
if let Some(name) = &event.restored_script_name {
self.restored_script_name = Some(name.clone());
}
match event.kind {
Some(pb::child_event::Kind::Print(print)) => {
let stream = match print.stream() {
pb::PrintStream::Stderr => PrintStream::Stderr,
pb::PrintStream::Stdout | pb::PrintStream::Unspecified => PrintStream::Stdout,
};
on_print(stream, &print.text).await;
}
Some(pb::child_event::Kind::FunctionCall(call)) => {
self.pending = Some(Pending::Call {
call_id: call.call_id,
function_name: call.function_name.clone(),
os_call: None,
});
return self.convert_turn(|| {
Ok(TurnEvent::FunctionCall {
function_name: call.function_name,
args: call.args,
kwargs: call.kwargs,
call_id: call.call_id,
object_id: call.object_id,
})
});
}
Some(pb::child_event::Kind::OsCall(call)) => {
let call_id = call.call_id;
let function_call = match call.call {
None => return Err(self.protocol_violation("OsCall event with no call")),
Some(kind) => match OsFunctionCall::try_from(kind) {
Ok(function_call) => function_call,
Err(err) => {
return Err(self.protocol_violation(format!("invalid OS call payload: {err}")));
}
},
};
let function_name = function_call.name().to_owned();
let (args, kwargs) = function_call.clone().to_args();
self.pending = Some(Pending::Call {
call_id,
function_name: function_name.clone(),
os_call: Some(Box::new(function_call)),
});
return Ok(ControlEvent::Turn(TurnEvent::OsCall {
function_name,
args,
kwargs,
call_id,
}));
}
Some(pb::child_event::Kind::NameLookup(lookup)) => {
let object_id = match lookup.object_id {
None => None,
Some(uuid) => match MontyUuid::try_from_slice(&uuid.data) {
Some(uuid) => Some(uuid),
None => {
return Err(self.protocol_violation("NameLookup.object_id is not a 16-byte uuid"));
}
},
};
self.pending = Some(Pending::NameLookup);
return Ok(ControlEvent::Turn(TurnEvent::NameLookup {
name: lookup.name,
object_id,
}));
}
Some(pb::child_event::Kind::ResolveFutures(futures)) => {
self.pending = Some(Pending::Futures);
return Ok(ControlEvent::Turn(TurnEvent::ResolveFutures {
pending_call_ids: futures.pending_call_ids,
}));
}
Some(pb::child_event::Kind::Complete(complete)) => {
self.pending = None;
self.feed_mounts = None;
return self.convert_turn(|| {
let value = complete
.value
.ok_or(monty_proto::ProtoConvertError::MissingField("Complete.value"))?;
Ok(TurnEvent::Complete(value.into_object()?))
});
}
Some(pb::child_event::Kind::Error(error)) => {
if !matches!(request.kind, Some(pb::parent_request::Kind::Dump(_))) {
self.pending = None;
self.feed_mounts = None;
}
let Some(exception) = error.exception else {
return Err(self.protocol_violation("error event with no exception"));
};
return match MontyException::try_from(exception) {
Ok(exc) => Err(PoolError::Runtime(exc)),
Err(err) => Err(self.protocol_violation(format!("invalid exception payload: {err}"))),
};
}
Some(pb::child_event::Kind::TypingError(typing)) => {
self.pending = None;
self.feed_mounts = None;
return Err(PoolError::Typing(typing.diagnostics));
}
Some(pb::child_event::Kind::Ok(_)) => return Ok(ControlEvent::Ok),
Some(pb::child_event::Kind::DumpResult(dump)) => return Ok(ControlEvent::Dump(dump.state)),
Some(pb::child_event::Kind::FatalError(fatal)) => {
return Err(self.fatal_error(&fatal.message).await);
}
Some(pb::child_event::Kind::Shutdown(shutdown)) => {
if !self.pool.config.transport.is_websocket() {
return Err(self.protocol_violation("subprocess worker sent a ShutdownDump"));
}
self.discard_worker();
return Err(PoolError::Shutdown { dump: shutdown.dump });
}
None => {
return Err(self.protocol_violation("unexpected event"));
}
}
}
}
fn convert_turn(
&mut self,
convert: impl FnOnce() -> Result<TurnEvent, monty_proto::ProtoConvertError>,
) -> Result<ControlEvent, PoolError> {
match convert() {
Ok(event) => Ok(ControlEvent::Turn(event)),
Err(err) => Err(self.protocol_violation(format!("invalid payload from worker: {err}"))),
}
}
fn build_feed_mounts(mounts: Vec<MountSpec>) -> Option<MountTable> {
(!mounts.is_empty()).then(|| build_mount_table(mounts))
}
async fn run_blocking<T: Send + 'static>(
&mut self,
work: impl FnOnce() -> T + Send + 'static,
) -> Result<T, PoolError> {
match spawn_blocking(work).await {
Ok(value) => Ok(value),
Err(err) => Err(self.protocol_violation(format!("blocking mount task failed: {err}"))),
}
}
fn protocol_violation(&mut self, context: impl Into<Cow<'static, str>>) -> PoolError {
self.discard_worker();
PoolError::Protocol(context.into())
}
async fn fatal_error(&mut self, message: &str) -> PoolError {
let status = self.reap_announced_exit().await;
self.pending = None;
self.feed_mounts = None;
PoolError::Crashed {
status,
cause: CrashCause::Announced {
reason: message.to_owned(),
},
}
}
async fn reap_announced_exit(&mut self) -> Option<ExitStatus> {
match self.worker.take() {
Some(mut worker) => {
#[cfg(feature = "telemetry")]
self.record_finish("error");
let _capacity = CapacityGuard::terminating(&self.pool, "fatal");
let status = worker.reap_or_kill(FATAL_EXIT_GRACE).await;
drop(worker);
status
}
None => None,
}
}
async fn poison(&mut self, context: &str) -> PoolError {
let Some(mut worker) = self.worker.take() else {
return PoolError::Finished;
};
#[cfg(feature = "telemetry")]
self.record_finish("error");
self.pending = None;
self.feed_mounts = None;
let websocket = self.pool.config.transport.is_websocket();
let mut capacity = CapacityGuard::terminating(&self.pool, if websocket { "disconnected" } else { "crash" });
let status = worker.reap_or_kill(FATAL_EXIT_GRACE).await;
drop(worker);
if websocket {
PoolError::Disconnected {
context: context.to_owned(),
}
} else if status.and_then(|status| status.code()) == Some(monty_types::OOM_EXIT_CODE) {
capacity.set_reason("oom");
PoolError::Runtime(MontyException::new(
ExcType::MemoryError,
Some("the worker exceeded its memory limit and was terminated".to_owned()),
))
} else {
PoolError::Crashed {
status,
cause: CrashCause::Vanished {
context: context.to_owned(),
},
}
}
}
async fn poison_timeout(&mut self) -> PoolError {
if let Some(mut worker) = self.worker.take() {
#[cfg(feature = "telemetry")]
self.record_finish("error");
let _capacity = CapacityGuard::terminating(&self.pool, "turn_timeout");
let _ = worker.kill_and_reap().await;
drop(worker);
}
self.pending = None;
self.feed_mounts = None;
PoolError::Timeout {
timeout: self.armed_deadline.unwrap_or(Duration::ZERO),
}
}
fn discard_worker(&mut self) {
if let Some(worker) = self.worker.take() {
#[cfg(feature = "telemetry")]
self.record_finish("error");
drop(worker);
self.pool.count_termination("discarded");
self.pool.release_capacity();
}
self.pending = None;
self.feed_mounts = None;
self.turn_in_flight = false;
self.abort_in_flight = false;
}
}
impl Drop for Checkout {
fn drop(&mut self) {
if let Some(worker) = self.worker.take() {
drop(worker);
self.pool.count_termination("abandoned");
self.pool.release_capacity();
}
#[cfg(feature = "telemetry")]
self.record_finish("abandoned");
}
}
fn flush_interval_ms(interval: Duration) -> u32 {
if interval.is_zero() {
0
} else {
u32::try_from(interval.as_millis()).unwrap_or(u32::MAX).max(1)
}
}
#[derive(Debug)]
enum ControlEvent {
Turn(TurnEvent),
Ok,
Dump(Vec<u8>),
}
const FATAL_EXIT_GRACE: Duration = Duration::from_millis(100);
pub(crate) fn request(kind: pb::parent_request::Kind) -> pb::ParentRequest {
pb::ParentRequest {
kind: Some(kind),
trace_parent: None,
}
}
fn ensure_sendable<'a>(values: impl IntoIterator<Item = &'a MontyObject>) -> Result<(), PoolError> {
if values.into_iter().any(exceeds_max_value_depth) {
Err(PoolError::Runtime(MontyException::new(
ExcType::RuntimeError,
Some("Max input depth exceeded".to_owned()),
)))
} else {
Ok(())
}
}
fn invalid_requirement(message: String) -> PoolError {
PoolError::Runtime(MontyException::new(ExcType::ValueError, Some(message)))
}
fn min_deadline(a: Option<Duration>, b: Option<Duration>) -> Option<Duration> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(deadline, None) | (None, deadline) => deadline,
}
}
fn build_mount_table(mounts: Vec<MountSpec>) -> MountTable {
let mut table = MountTable::new();
for mount in mounts {
let mode = match mount.mode {
MountSpecMode::ReadOnly => MountMode::ReadOnly,
MountSpecMode::ReadWrite => MountMode::ReadWrite,
MountSpecMode::Overlay => MountMode::OverlayMemory(OverlayState::new()),
};
let mount = monty_fs::Mount::with_root(mount.root, mode, mount.write_bytes_limit)
.with_memory_usage_limit(mount.memory_usage_limit);
table.push_mount(mount);
}
table
}