#[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, ext_result_to_proto, future_results_to_proto, named_values_to_proto,
os_call_from_proto, pb, validate_requirement,
};
use monty_types::{
AssertMessageAnnotations, CallArgs, DEFAULT_MAX_SUSPENSIONS, ExcType, ExtFunctionResult, IMPORT_STUBS_CALL_ID,
MONTY_VERSION, MontyException, MontyObject, MontyUuid, NameLookupResult, NamedValues, OsFunctionCall, OsPolicy,
PrintStream, ResourceLimits, SleepMode, SourceRange, TypeCheckingConfig, validate_cwd,
};
#[cfg(feature = "telemetry")]
use opentelemetry::trace::{FutureExt, TraceContextExt};
use tokio::{task::spawn_blocking, time::timeout};
#[cfg(feature = "telemetry")]
use crate::telemetry::{TelemetryContext, metrics::outcome};
use crate::{
CrashCause, PoolConfig, 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>,
pub os_policy: OsPolicy,
pub persistence: Persistence,
pub profile: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Persistence {
#[default]
ServerDefault,
Ephemeral,
Stored,
}
impl From<Persistence> for pb::Persistence {
fn from(persistence: Persistence) -> Self {
match persistence {
Persistence::ServerDefault => Self::Unspecified,
Persistence::Ephemeral => Self::Ephemeral,
Persistence::Stored => Self::Stored,
}
}
}
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,
os_policy: OsPolicy::default(),
persistence: Persistence::default(),
profile: 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: CallArgs,
call_id: u32,
object_id: Option<MontyUuid>,
allow_eager_await: bool,
position: SourceRange,
},
OsCall {
function_name: String,
args: CallArgs,
call_id: u32,
allow_eager_await: bool,
system_sleep: Option<Duration>,
position: SourceRange,
},
NameLookup {
name: String,
object_id: Option<MontyUuid>,
position: SourceRange,
},
ResolveFutures {
pending_call_ids: Vec<u32>,
position: SourceRange,
},
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(()))
}
}
#[expect(
clippy::struct_excessive_bools,
reason = "independent turn, abort, cwd and send flags"
)]
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>,
session_id: Option<Vec<u8>>,
redial: Option<Box<Redial>>,
feed_mounts: Option<MountTable>,
cwd_set: bool,
request_sent: bool,
#[cfg(feature = "telemetry")]
started: Option<Instant>,
}
#[derive(Clone, Copy)]
struct SessionBudget {
feed_budget: Option<Duration>,
turn_budget: Option<Duration>,
reported_feed_execution: Duration,
suspension_limit: u64,
suspensions_seen: u64,
sleep_limit: Option<Duration>,
system_sleep_max: Duration,
sleep_used: Duration,
}
impl SessionBudget {
fn from_config(repl: &ReplConfig) -> Self {
let limits = repl.limits.as_ref();
Self {
feed_budget: limits.and_then(|limits| limits.max_feed_duration),
turn_budget: limits.and_then(|limits| limits.max_turn_duration),
reported_feed_execution: Duration::ZERO,
suspension_limit: limits.map_or(DEFAULT_MAX_SUSPENSIONS as u64, |limits| limits.max_suspensions as u64),
suspensions_seen: 0,
sleep_limit: limits.and_then(|limits| limits.max_total_sleep),
sleep_used: Duration::ZERO,
system_sleep_max: match repl.os_policy.sleep {
SleepMode::System(max) => max,
SleepMode::CallHost | SleepMode::Zero => SleepMode::DEFAULT_MAX,
},
}
}
fn forget(&mut self) {
*self = Self {
feed_budget: self.feed_budget,
turn_budget: self.turn_budget,
reported_feed_execution: Duration::ZERO,
suspension_limit: self.suspension_limit,
suspensions_seen: 0,
sleep_limit: self.sleep_limit,
sleep_used: Duration::ZERO,
system_sleep_max: self.system_sleep_max,
};
}
fn update_from(&mut self, event: &pb::ChildEvent) {
self.reported_feed_execution = self
.reported_feed_execution
.max(Duration::from_micros(event.feed_execution_micros));
self.feed_budget = tighter(self.feed_budget, event.max_feed_duration_micros);
self.turn_budget = tighter(self.turn_budget, event.max_turn_duration_micros);
self.sleep_limit = tighter(self.sleep_limit, event.max_total_sleep_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 begin_feed(&mut self) {
self.reported_feed_execution = Duration::ZERO;
}
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 charge_sleep(&mut self, event: &pb::ChildEvent) -> Option<(Duration, Duration)> {
let seconds = match &event.kind {
Some(pb::child_event::Kind::OsCall(call)) => match call.call {
Some(pb::os_call::Call::SystemSleep(pb::os_call::Sleep { seconds })) => seconds,
Some(pb::os_call::Call::AsyncSystemSleep(pb::os_call::AsyncSleep { delay })) => delay,
_ => return None,
},
_ => return None,
};
let delay = self.cap_system_sleep(Duration::try_from_secs_f64(seconds).unwrap_or(Duration::MAX));
let total = self.sleep_used.saturating_add(delay);
match self.sleep_limit {
Some(limit) if total > limit => Some((limit, total)),
_ => {
self.sleep_used = total;
None
}
}
}
fn cap_system_sleep(&self, delay: Duration) -> Duration {
delay.min(self.system_sleep_max)
}
fn backstop_deadline(&self, graces: DurationGraces) -> Option<Duration> {
[
remaining_deadline(self.feed_budget, self.reported_feed_execution, graces.feed),
remaining_deadline(self.turn_budget, Duration::ZERO, graces.turn),
]
.into_iter()
.flatten()
.min()
}
}
fn tighter(limit: Option<Duration>, reported_micros: Option<u64>) -> Option<Duration> {
match (limit, reported_micros.map(Duration::from_micros)) {
(Some(limit), Some(reported)) => Some(limit.min(reported)),
(limit, reported) => limit.or(reported),
}
}
#[derive(Clone, Copy)]
struct DurationGraces {
feed: Option<Duration>,
turn: Option<Duration>,
}
impl DurationGraces {
fn from_config(config: &PoolConfig) -> Self {
Self {
feed: config.feed_duration_limit_grace,
turn: config.turn_duration_limit_grace,
}
}
}
fn remaining_deadline(budget: Option<Duration>, consumed: Duration, grace: Option<Duration>) -> Option<Duration> {
Some(budget?.saturating_sub(consumed).saturating_add(grace?))
}
fn is_execution_turn(request: &pb::ParentRequest) -> bool {
matches!(
request.kind,
Some(
pb::parent_request::Kind::Feed(_)
| pb::parent_request::Kind::ResumeCall(_)
| pb::parent_request::Kind::ResumeNameLookup(_)
| pb::parent_request::Kind::ResumeFutures(_)
)
)
}
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")),
)
}
fn sleep_limit_exceeded(limit: Duration, total: Duration) -> MontyException {
MontyException::new(
ExcType::TimeoutError,
Some(format!("sleep limit exceeded: {total:?} > {limit:?}")),
)
}
enum Pending {
Call {
call_id: u32,
function_name: String,
os_call: Option<Box<OsFunctionCall>>,
allow_eager_await: bool,
},
NameLookup { name: String, object_id: Option<MontyUuid> },
Futures { pending_call_ids: Vec<u32> },
}
#[derive(Debug, PartialEq, Eq)]
enum PendingKey {
Call { call_id: u32, function_name: String },
NameLookup { name: String, object_id: Option<MontyUuid> },
Futures { pending_call_ids: Vec<u32> },
}
impl PendingKey {
fn of(pending: &Pending) -> Self {
match pending {
Pending::Call {
call_id, function_name, ..
} => Self::Call {
call_id: *call_id,
function_name: function_name.clone(),
},
Pending::NameLookup { name, object_id } => Self::NameLookup {
name: name.clone(),
object_id: *object_id,
},
Pending::Futures { pending_call_ids } => Self::Futures {
pending_call_ids: pending_call_ids.clone(),
},
}
}
}
pub(crate) struct Redial {
pub(crate) repl: ReplConfig,
pub(crate) connect_headers: Vec<(String, String)>,
#[cfg(feature = "telemetry")]
pub(crate) telemetry: Option<TelemetryContext>,
}
impl Checkout {
pub(crate) async fn create(
worker: Worker,
pool: Arc<PoolInner>,
repl: &ReplConfig,
redial: Option<Redial>,
) -> Result<Self, PoolError> {
let request = configure_request(repl);
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,
session_id: None,
redial: redial.map(Box::new),
feed_mounts: None,
cwd_set: false,
request_sent: false,
#[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;
let session_id = self.session_id.take();
self.feed_mounts = feed_mounts;
let request = request(pb::parent_request::Kind::Load(pb::Load { state: state.into() }));
let outcome = self
.request_turn(&request, self.pool.config.request_timeout, on_print)
.await;
self.end_load();
if matches!(outcome, Err(PoolError::Runtime(_))) && self.session_id.is_none() {
self.session_id = session_id;
}
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:?}")));
}
};
self.cwd_set = true;
Ok((event, self.restored_script_name.take()))
}
pub async fn feed(
&mut self,
code: impl Into<String>,
inputs: impl Into<NamedValues>,
mounts: Vec<MountSpec>,
skip_type_check: bool,
on_print: OnPrint<'_>,
) -> Result<TurnEvent, PoolError> {
self.feed_with_cwd(code, inputs, mounts, None, skip_type_check, on_print)
.await
}
pub async fn feed_with_cwd(
&mut self,
code: impl Into<String>,
inputs: impl Into<NamedValues>,
mounts: Vec<MountSpec>,
cwd: Option<&str>,
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(),
));
}
let cwd = match cwd {
Some(cwd) => checked_cwd(cwd)?,
None if self.cwd_set => String::new(),
None => mounts
.first()
.map_or_else(|| "/".to_owned(), |mount| mount.virtual_path().to_owned()),
};
self.feed_mounts = Self::build_feed_mounts(mounts)?;
let (inputs, values) = named_values_to_proto(inputs.into());
self.budget.begin_feed();
let request = request(pb::parent_request::Kind::Feed(pb::Feed {
code: code.into(),
inputs,
values: Some(values),
skip_type_check,
cwd,
}));
let outcome = self.expect_turn(&request, on_print).await;
if self.request_sent {
self.cwd_set = true;
}
outcome
}
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(),
));
}
let (result, values) = match value {
ResumeValue::Return(value) => ext_result_to_proto(ExtFunctionResult::Return(value)),
ResumeValue::Error(exc) => ext_result_to_proto(ExtFunctionResult::Error(exc)),
ResumeValue::Future => ext_result_to_proto(ExtFunctionResult::Future(call_id)),
ResumeValue::NotFound => ext_result_to_proto(ExtFunctionResult::NotFound(function_name)),
ResumeValue::NotHandled => (
pb::ExtFunctionResult {
kind: Some(pb::ext_function_result::Kind::NotHandled(pb::Unit {})),
},
None,
),
};
let request = request(pb::parent_request::Kind::ResumeCall(pb::ResumeCall {
call_id,
result: Some(result),
values,
}));
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 request = request(pb::parent_request::Kind::ResumeNameLookup(result.into().into()));
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()?;
match &self.pending {
Some(Pending::Call {
call_id,
allow_eager_await: true,
..
}) => {
if results.len() != 1 || results[0].0 != *call_id {
return Err(PoolError::Protocol(
"eager result must match the suspended call id".into(),
));
}
}
Some(Pending::Futures { .. }) => {}
_ => return Err(PoolError::Protocol("no suspended futures to resume".into())),
}
let results = results
.into_iter()
.map(|(call_id, value)| {
let result = match value {
ResumeValue::Return(value) => ExtFunctionResult::Return(value),
ResumeValue::Error(exc) => ExtFunctionResult::Error(exc),
ResumeValue::Future | ResumeValue::NotFound | ResumeValue::NotHandled => {
return Err(PoolError::Protocol(
format!("future {call_id} must resolve to Return or Error").into(),
));
}
};
Ok((call_id, result))
})
.collect::<Result<Vec<_>, _>>()?;
let request = request(pb::parent_request::Kind::ResumeFutures(future_results_to_proto(
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: requirements.into(),
}));
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);
}
}
#[must_use]
pub fn worker_id(&self) -> Option<u64> {
self.worker.as_ref().map(|worker| worker.id)
}
pub fn pid(&self) -> Option<u32> {
self.worker.as_ref().and_then(Worker::pid)
}
#[must_use]
pub fn session_id(&self) -> Option<&[u8]> {
self.session_id.as_deref()
}
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(DurationGraces::from_config(&self.pool.config))
}
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: &mut 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 exceeded = self
.budget
.over_suspension_limit(event)
.map(suspension_limit_exceeded)
.or_else(|| {
self.budget
.charge_sleep(event)
.map(|(limit, total)| sleep_limit_exceeded(limit, total))
});
let Some(exception) = exceeded else {
return Ok(false);
};
if let Some(pb::child_event::Kind::OsCall(call)) = &mut event.kind
&& let Err(err) = os_call_from_proto(mem::take(call))
{
return Err(self.protocol_violation(format!("invalid OS call payload: {err}")));
}
let abort = request(pb::parent_request::Kind::AbortFeed(pb::AbortFeed {
exception: Some((&exception).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, &mut *on_print)).await {
Ok(outcome) => outcome,
Err(_elapsed) => Err(self.poison_timeout().await),
},
None => self.turn_io(request, &mut *on_print).await,
};
self.turn_in_flight = false;
match outcome {
Err(PoolError::Shutdown { dump }) => Box::pin(self.after_shutdown(dump, request, deadline, on_print)).await,
outcome => outcome,
}
}
async fn after_shutdown(
&mut self,
dump: Option<Vec<u8>>,
request: &pb::ParentRequest,
deadline: Option<Duration>,
on_print: OnPrint<'_>,
) -> Result<ControlEvent, PoolError> {
let outcome = match dump {
Some(state) if self.can_resume(request) => {
self.resume_after_shutdown(state, request, deadline, on_print).await
}
dump => Err(PoolError::Shutdown { dump }),
};
if matches!(outcome, Err(PoolError::Shutdown { .. })) {
#[cfg(feature = "telemetry")]
self.record_finish("error");
self.discard_worker();
}
outcome
}
#[cold]
fn can_resume(&self, request: &pb::ParentRequest) -> bool {
self.redial.is_some()
&& self.session_id.is_some()
&& !matches!(
request.kind,
Some(
pb::parent_request::Kind::Configure(_)
| pb::parent_request::Kind::Load(_)
| pb::parent_request::Kind::Reset(_)
| pb::parent_request::Kind::Shutdown(_)
)
)
}
async fn resume_after_shutdown(
&mut self,
state: Vec<u8>,
request: &pb::ParentRequest,
deadline: Option<Duration>,
on_print: OnPrint<'_>,
) -> Result<ControlEvent, PoolError> {
let redial = self.redial.take().expect("checked by can_resume");
let reloaded = self.reload_session(&redial, &state).await;
#[cfg(feature = "telemetry")]
if let Some(metrics) = &self.pool.config.metrics {
metrics.session_resumed(reloaded.as_ref().map_or_else(resume_failure, |()| "ok"));
}
let outcome = match reloaded {
Ok(()) => {
if matches!(request.kind, Some(pb::parent_request::Kind::Feed(_))) {
self.budget.begin_feed();
}
let deadline = if is_execution_turn(request) {
min_deadline(self.pool.config.request_timeout, self.backstop_deadline())
} else {
deadline
};
self.request_turn(request, deadline, on_print).await
}
Err(_) => Err(PoolError::Shutdown { dump: Some(state) }),
};
self.redial = Some(redial);
outcome
}
async fn reload_session(&mut self, redial: &Redial, state: &[u8]) -> Result<(), PoolError> {
let configure = configure_request(&redial.repl);
let worker = self.pool.acquire_worker(&redial.connect_headers).await?;
#[cfg(feature = "telemetry")]
let worker = worker.with_adapter_context(redial.telemetry.clone());
self.worker = Some(worker);
let expected = self.pending.as_ref().map(PendingKey::of);
let timeout = self.pool.config.request_timeout;
let mut no_print = on_print_sync(|_, _| {});
match self.request_turn(&configure, timeout, &mut no_print).await? {
ControlEvent::Ok => {}
other => return Err(self.protocol_violation(format!("unexpected reply to Configure: {other:?}"))),
}
let (suspensions_seen, sleep_used) = (self.budget.suspensions_seen, self.budget.sleep_used);
self.budget.suspensions_seen = 0;
self.budget.sleep_used = Duration::ZERO;
let load = request(pb::parent_request::Kind::Load(pb::Load {
state: state.to_vec().into(),
}));
let reply = self.request_turn(&load, timeout, &mut no_print).await;
self.restored_script_name = None;
let matches = match reply? {
ControlEvent::Ok => expected.is_none(),
ControlEvent::Turn(_) => expected.is_some() && expected == self.pending.as_ref().map(PendingKey::of),
ControlEvent::Dump(_) => false,
};
self.budget.suspensions_seen = suspensions_seen;
self.budget.sleep_used = sleep_used;
if matches {
#[cfg(feature = "telemetry")]
if let Some(worker) = &mut self.worker {
worker.mark_resumed();
}
Ok(())
} else {
Err(self.protocol_violation("the resumed session does not match the one shut down"))
}
}
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();
}
if matches!(request.kind, Some(pb::parent_request::Kind::Feed(_))) {
self.budget.begin_feed();
}
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> {
self.send_request(request).await?;
loop {
let mut event = match self.worker.as_mut().expect("checked by send_request").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(&mut 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(())
}
}
#[cfg(feature = "telemetry")]
pub fn callback_context(&self) -> opentelemetry::Context {
self.worker
.as_ref()
.map_or_else(opentelemetry::Context::new, Worker::callback_context)
}
async fn turn_io(&mut self, request: &pb::ParentRequest, on_print: OnPrint<'_>) -> Result<ControlEvent, PoolError> {
self.send_request(request).await?;
loop {
let mut event = match self.worker.as_mut().expect("checked by send_request").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 let Some(session_id) = event.session_id.take() {
self.session_id = Some(session_id.into_inner());
}
if self.abort_if_over_budget(&mut 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)) => {
#[cfg(feature = "telemetry")]
let context = self.callback_context();
for segment in &print.segments {
let stream = match segment.stream() {
pb::PrintStream::Stderr => PrintStream::Stderr,
pb::PrintStream::Stdout | pb::PrintStream::Unspecified => PrintStream::Stdout,
};
let future = {
#[cfg(feature = "telemetry")]
let _context = context.has_active_span().then(|| context.clone().attach());
on_print(stream, &segment.text)
};
#[cfg(feature = "telemetry")]
if context.has_active_span() {
future.with_context(context.clone()).await;
} else {
future.await;
}
#[cfg(not(feature = "telemetry"))]
future.await;
}
}
Some(pb::child_event::Kind::FunctionCall(mut call)) => {
let position = suspension_position(call.position.take());
self.pending = Some(Pending::Call {
call_id: call.call_id,
function_name: call.function_name.clone(),
os_call: None,
allow_eager_await: call.allow_eager_await,
});
return self.convert_turn(|| {
Ok(TurnEvent::FunctionCall {
function_name: call.function_name.clone(),
call_id: call.call_id,
object_id: call.object_id,
allow_eager_await: call.allow_eager_await,
args: call.into_call_args()?,
position,
})
});
}
Some(pb::child_event::Kind::OsCall(mut call)) => {
let position = suspension_position(call.position.take());
let mut allow_eager_await = call.allow_eager_await;
let (call_id, function_call) = match os_call_from_proto(call) {
Ok(call) => call,
Err(err) => {
return Err(self.protocol_violation(format!("invalid OS call payload: {err}")));
}
};
let function_name = function_call.name().to_owned();
allow_eager_await = allow_eager_await && OsFunctionCall::accepts_future(function_call.name());
let system_sleep = match function_call {
OsFunctionCall::SystemSleep(delay) | OsFunctionCall::AsyncSystemSleep(delay) => {
Some(self.budget.cap_system_sleep(delay))
}
_ => None,
};
let args = function_call.clone().to_args();
self.pending = Some(Pending::Call {
call_id,
function_name: function_name.clone(),
os_call: Some(Box::new(function_call)),
allow_eager_await,
});
return Ok(ControlEvent::Turn(TurnEvent::OsCall {
function_name,
args,
call_id,
allow_eager_await,
system_sleep,
position,
}));
}
Some(pb::child_event::Kind::NameLookup(lookup)) => {
let position = suspension_position(lookup.position);
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 {
name: lookup.name.clone(),
object_id,
});
return Ok(ControlEvent::Turn(TurnEvent::NameLookup {
name: lookup.name,
object_id,
position,
}));
}
Some(pb::child_event::Kind::ResolveFutures(futures)) => {
let position = suspension_position(futures.position);
let pending_call_ids = futures.pending_call_ids.into_inner();
self.pending = Some(Pending::Futures {
pending_call_ids: pending_call_ids.clone(),
});
return Ok(ControlEvent::Turn(TurnEvent::ResolveFutures {
pending_call_ids,
position,
}));
}
Some(pb::child_event::Kind::Complete(complete)) => {
self.pending = None;
self.feed_mounts = None;
return self.convert_turn(|| Ok(TurnEvent::Complete(MontyObject::try_from(complete)?)));
}
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)) => {
let type_checked = match &request.kind {
Some(pb::parent_request::Kind::Feed(feed)) => !feed.skip_type_check,
Some(pb::parent_request::Kind::ResumeCall(resume)) => resume.call_id == IMPORT_STUBS_CALL_ID,
_ => false,
};
if !type_checked {
return Err(
self.protocol_violation("TypingError reply to a request that is not a type-checked Feed")
);
}
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.into_inner()));
}
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.release_worker();
return Err(PoolError::Shutdown {
dump: shutdown.dump.map(Into::into),
});
}
None => {
return Err(self.protocol_violation("unexpected event"));
}
}
}
}
async fn send_request(&mut self, request: &pb::ParentRequest) -> Result<(), PoolError> {
self.request_sent = false;
let Some(worker) = self.worker.as_mut() else {
return Err(PoolError::Finished);
};
match worker.send(request).await {
Ok(()) => {
self.request_sent = true;
Ok(())
}
Err(FrameError::FrameTooLarge { len, max }) => Err(PoolError::Runtime(MontyException::new(
ExcType::RuntimeError,
Some(format!(
"request frame of {len} bytes exceeds the maximum of {max} bytes"
)),
))),
Err(_) => Err(self.poison("sending a request").await),
}
}
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>) -> Result<Option<MountTable>, PoolError> {
(!mounts.is_empty()).then(|| build_mount_table(mounts)).transpose()
}
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) {
#[cfg(feature = "telemetry")]
if self.worker.is_some() {
self.record_finish("error");
}
self.release_worker();
self.pending = None;
self.feed_mounts = None;
self.turn_in_flight = false;
self.abort_in_flight = false;
}
fn release_worker(&mut self) {
if let Some(worker) = self.worker.take() {
drop(worker);
self.pool.count_termination("discarded");
self.pool.release_capacity();
}
}
}
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 configure_request(repl: &ReplConfig) -> pb::ParentRequest {
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),
os_policy: Some((&repl.os_policy).into()),
persistence: pb::Persistence::from(repl.persistence).into(),
profile: repl.profile.clone(),
}))
}
#[cfg(feature = "telemetry")]
fn resume_failure(err: &PoolError) -> &'static str {
match err {
PoolError::Exhausted => "exhausted",
PoolError::Disconnected { .. } | PoolError::Spawn(_) => "disconnected",
PoolError::Crashed { .. } | PoolError::Runtime(_) | PoolError::Typing(_) => "refused",
PoolError::Shutdown { .. } => "shutdown",
PoolError::Protocol(_) => "mismatch",
PoolError::Timeout { .. } => "timeout",
PoolError::Finished => "error",
}
}
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 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 checked_cwd(cwd: &str) -> Result<String, PoolError> {
validate_cwd(cwd).map_err(|message| PoolError::Runtime(MontyException::new(ExcType::ValueError, Some(message))))
}
fn build_mount_table(mounts: Vec<MountSpec>) -> Result<MountTable, PoolError> {
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)
.map_err(|err| PoolError::Runtime(err.into_exception()))?;
}
Ok(table)
}
fn suspension_position(position: Option<impl Into<SourceRange>>) -> SourceRange {
position.map_or_else(SourceRange::unknown, Into::into)
}