2026-08-27 22:09:26 +00:00
|
|
|
use std::collections::{BTreeMap, HashMap};
|
2026-08-28 09:07:31 +00:00
|
|
|
use std::fs::{self, File};
|
|
|
|
|
use std::io::Write;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2026-08-27 22:09:26 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2026-08-28 09:07:31 +00:00
|
|
|
use data_plane::blob_transfer::{
|
|
|
|
|
BlobTransferEvent, BlobTransferId, BlobTransferOffer, BlobTransferReceiver, BlobTransferSender,
|
|
|
|
|
};
|
2026-08-27 22:09:26 +00:00
|
|
|
use data_plane::host::HostRouteRegistrar;
|
2026-08-28 09:07:31 +00:00
|
|
|
use data_plane::namespace::{
|
|
|
|
|
BlobBinding, DataDirectoryOut, NamespaceClient, NamespaceClientIn, NamespaceRequest,
|
|
|
|
|
};
|
|
|
|
|
use data_plane::source::{BlobSourceIn, BlobSourcePublisher};
|
2026-08-27 22:09:26 +00:00
|
|
|
use data_plane::stream_transport::StreamTransport;
|
|
|
|
|
use distribution::transport_bridge::{OutboxRouteBinder, RouteBinder, RouteView};
|
|
|
|
|
use distribution::types::NodeId;
|
|
|
|
|
use iroh::EndpointAddr;
|
|
|
|
|
use swactor::actor::ActorAddress;
|
|
|
|
|
use swactor::runtime::Runtime;
|
|
|
|
|
use swactor_engine::EngineHandle;
|
|
|
|
|
use swactor_process::{ExitStatus, ProcessOutput, ProcessSpec};
|
|
|
|
|
use swactor_process_context::{
|
|
|
|
|
BootstrapFailure, ContextualProcessCommand, ContextualProcessOutput,
|
|
|
|
|
ContextualProcessOutputConfig, ContextualProcessSpec, ExecutionIdentity,
|
|
|
|
|
send_contextual_process_command,
|
|
|
|
|
};
|
|
|
|
|
use swactor_process_context::{
|
|
|
|
|
ContextProvisioner, ContextualProcessSpawner, DataPlaneProvisioner, DataPlaneProvisionerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pub struct MyelinChildRouteRegistrar {
|
|
|
|
|
route_view: RouteView,
|
|
|
|
|
pinned_routes: RouteView,
|
|
|
|
|
route_binder: Arc<OutboxRouteBinder>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MyelinChildRouteRegistrar {
|
|
|
|
|
pub fn new(
|
|
|
|
|
route_view: RouteView,
|
|
|
|
|
pinned_routes: RouteView,
|
|
|
|
|
route_binder: Arc<OutboxRouteBinder>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
route_view,
|
|
|
|
|
pinned_routes,
|
|
|
|
|
route_binder,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn register_route(&self, actor: ActorAddress, node_bytes: [u8; 32]) -> Result<(), String> {
|
|
|
|
|
let node = NodeId(node_bytes);
|
|
|
|
|
self.pinned_routes
|
|
|
|
|
.write()
|
|
|
|
|
.map_err(|_| "pinned route view is poisoned".to_owned())?
|
|
|
|
|
.insert(actor, node);
|
|
|
|
|
self.route_view
|
|
|
|
|
.write()
|
|
|
|
|
.map_err(|_| "route view is poisoned".to_owned())?
|
|
|
|
|
.insert(actor, node);
|
|
|
|
|
self.route_binder.ensure_routable(actor);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn revoke_route(&self, actor: ActorAddress) -> Result<(), String> {
|
|
|
|
|
self.pinned_routes
|
|
|
|
|
.write()
|
|
|
|
|
.map_err(|_| "pinned route view is poisoned".to_owned())?
|
|
|
|
|
.remove(&actor);
|
|
|
|
|
self.route_view
|
|
|
|
|
.write()
|
|
|
|
|
.map_err(|_| "route view is poisoned".to_owned())?
|
|
|
|
|
.remove(&actor);
|
|
|
|
|
self.route_binder.remove_route(&actor);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HostRouteRegistrar for MyelinChildRouteRegistrar {
|
|
|
|
|
fn register_child(
|
|
|
|
|
&self,
|
|
|
|
|
child_session: ActorAddress,
|
|
|
|
|
child_node: [u8; 32],
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
self.register_route(child_session, child_node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn revoke_child(&self, child_session: ActorAddress) -> Result<(), String> {
|
|
|
|
|
self.revoke_route(child_session)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_routable(&self, actor: ActorAddress) -> bool {
|
|
|
|
|
self.pinned_routes
|
|
|
|
|
.read()
|
|
|
|
|
.is_ok_and(|routes| routes.contains_key(&actor))
|
|
|
|
|
|| self
|
|
|
|
|
.route_view
|
|
|
|
|
.read()
|
|
|
|
|
.is_ok_and(|routes| routes.contains_key(&actor))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct MyelinContextualProcessConfig {
|
|
|
|
|
pub runtime: Runtime,
|
|
|
|
|
pub engine: EngineHandle,
|
|
|
|
|
pub arena_bytes: u64,
|
|
|
|
|
pub arena_alignment: u64,
|
|
|
|
|
pub namespace: Option<NamespaceClient>,
|
|
|
|
|
pub transfer_receiver: Option<Arc<dyn BlobTransferReceiver>>,
|
|
|
|
|
pub source_sender: Option<Arc<dyn BlobTransferSender>>,
|
|
|
|
|
pub source_publisher: Option<Arc<dyn BlobSourcePublisher>>,
|
|
|
|
|
pub route_view: RouteView,
|
|
|
|
|
pub pinned_routes: RouteView,
|
|
|
|
|
pub route_binder: Arc<OutboxRouteBinder>,
|
|
|
|
|
pub stream_transport: Option<Arc<dyn StreamTransport>>,
|
|
|
|
|
pub host_endpoint: EndpointAddr,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn build_contextual_process_spawner(
|
|
|
|
|
config: MyelinContextualProcessConfig,
|
|
|
|
|
) -> Result<ContextualProcessSpawner, String> {
|
|
|
|
|
let routing = serde_json::to_vec(&config.host_endpoint)
|
|
|
|
|
.map_err(|error| format!("serialize contextual routing material: {error}"))?;
|
|
|
|
|
let registrar: Arc<dyn HostRouteRegistrar> = Arc::new(MyelinChildRouteRegistrar::new(
|
|
|
|
|
config.route_view,
|
|
|
|
|
config.pinned_routes,
|
|
|
|
|
config.route_binder,
|
|
|
|
|
));
|
|
|
|
|
let provisioner: Arc<dyn ContextProvisioner> =
|
|
|
|
|
Arc::new(DataPlaneProvisioner::new(DataPlaneProvisionerConfig {
|
|
|
|
|
runtime: config.runtime,
|
|
|
|
|
engine: config.engine.clone(),
|
|
|
|
|
arena_bytes: config.arena_bytes,
|
|
|
|
|
arena_alignment: config.arena_alignment,
|
|
|
|
|
namespace: config.namespace,
|
|
|
|
|
transfer_receiver: config.transfer_receiver,
|
|
|
|
|
source_sender: config.source_sender,
|
|
|
|
|
source_publisher: config.source_publisher,
|
|
|
|
|
route_registrar: Some(registrar),
|
|
|
|
|
stream_transport: config.stream_transport,
|
|
|
|
|
routing: Arc::new(move |_| Ok(routing.clone())),
|
|
|
|
|
})?);
|
|
|
|
|
Ok(ContextualProcessSpawner::new(config.engine, provisioner))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 09:07:31 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub(crate) struct ContextualProgramFileWire {
|
|
|
|
|
pub namespace_path: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 22:09:26 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub(crate) struct ContextualProcessSpecWire {
|
|
|
|
|
pub command: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub args: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub env: BTreeMap<String, String>,
|
|
|
|
|
pub working_dir: Option<String>,
|
|
|
|
|
pub label: Option<String>,
|
|
|
|
|
pub execution_id: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub read_prefixes: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub write_prefixes: Vec<String>,
|
|
|
|
|
pub attach_timeout_ms: u64,
|
2026-08-28 09:07:31 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub staged_program: Option<ContextualProgramFileWire>,
|
2026-08-27 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContextualProcessSpecWire {
|
|
|
|
|
fn into_spec(self) -> Result<ContextualProcessSpec, String> {
|
|
|
|
|
let parse_paths = |paths: Vec<String>| {
|
|
|
|
|
paths
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(data_plane::path::DataPath::parse)
|
|
|
|
|
.collect::<Result<Vec<_>, _>>()
|
|
|
|
|
.map_err(|error| error.to_string())
|
|
|
|
|
};
|
|
|
|
|
Ok(ContextualProcessSpec {
|
|
|
|
|
process: ProcessSpec {
|
|
|
|
|
command: self.command,
|
|
|
|
|
args: self.args,
|
|
|
|
|
env: self.env.into_iter().collect(),
|
|
|
|
|
working_dir: self.working_dir.map(PathBuf::from),
|
|
|
|
|
label: self.label,
|
|
|
|
|
},
|
|
|
|
|
access: data_plane::path::SessionAccess {
|
|
|
|
|
execution_id: self.execution_id,
|
|
|
|
|
read_prefixes: parse_paths(self.read_prefixes)?,
|
|
|
|
|
write_prefixes: parse_paths(self.write_prefixes)?,
|
|
|
|
|
},
|
|
|
|
|
attach_deadline: Duration::from_millis(self.attach_timeout_ms),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub(crate) enum ContextualNodeCommand {
|
|
|
|
|
Spawn {
|
|
|
|
|
request_id: String,
|
|
|
|
|
spec: ContextualProcessSpecWire,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
},
|
|
|
|
|
Stop {
|
|
|
|
|
request_id: String,
|
|
|
|
|
process: ActorAddress,
|
|
|
|
|
kill_after_ms: Option<u64>,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
},
|
|
|
|
|
Query {
|
|
|
|
|
request_id: String,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub(crate) struct ExecutionIdentityWire {
|
|
|
|
|
pub execution_id: u64,
|
|
|
|
|
pub generation: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<ExecutionIdentity> for ExecutionIdentityWire {
|
|
|
|
|
fn from(identity: ExecutionIdentity) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
execution_id: identity.execution_id,
|
|
|
|
|
generation: identity.generation,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
|
|
|
|
pub(crate) enum ContextualExitStatusWire {
|
|
|
|
|
Code(i32),
|
|
|
|
|
Signal(i32),
|
|
|
|
|
Unknown,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<ExitStatus> for ContextualExitStatusWire {
|
|
|
|
|
fn from(status: ExitStatus) -> Self {
|
|
|
|
|
match status {
|
|
|
|
|
ExitStatus::Code(code) => Self::Code(code),
|
|
|
|
|
ExitStatus::Signal(signal) => Self::Signal(signal),
|
|
|
|
|
ExitStatus::Unknown => Self::Unknown,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub(crate) struct LiveContextualExecutionWire {
|
|
|
|
|
pub request_id: String,
|
|
|
|
|
pub process: ActorAddress,
|
|
|
|
|
pub identity: ExecutionIdentityWire,
|
|
|
|
|
pub started_pid: Option<u32>,
|
|
|
|
|
pub context_ready: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
|
|
|
pub(crate) enum ContextualProcessEventKindWire {
|
|
|
|
|
Spawned {
|
|
|
|
|
process: ActorAddress,
|
|
|
|
|
identity: ExecutionIdentityWire,
|
|
|
|
|
},
|
|
|
|
|
SpawnRejected {
|
|
|
|
|
error: String,
|
|
|
|
|
},
|
|
|
|
|
ProcessStarted {
|
|
|
|
|
pid: u32,
|
|
|
|
|
},
|
|
|
|
|
ContextReady,
|
|
|
|
|
Stdout {
|
|
|
|
|
bytes: Vec<u8>,
|
|
|
|
|
},
|
|
|
|
|
Stderr {
|
|
|
|
|
bytes: Vec<u8>,
|
|
|
|
|
},
|
|
|
|
|
SpawnFailed {
|
|
|
|
|
error: String,
|
|
|
|
|
},
|
|
|
|
|
BootstrapFailed {
|
|
|
|
|
error: String,
|
|
|
|
|
},
|
|
|
|
|
Exited {
|
|
|
|
|
status: ContextualExitStatusWire,
|
|
|
|
|
},
|
|
|
|
|
ProcessError {
|
|
|
|
|
error: String,
|
|
|
|
|
},
|
|
|
|
|
StopAccepted {
|
|
|
|
|
process: ActorAddress,
|
|
|
|
|
},
|
|
|
|
|
StopRejected {
|
|
|
|
|
error: String,
|
|
|
|
|
},
|
|
|
|
|
LiveExecutions {
|
|
|
|
|
executions: Vec<LiveContextualExecutionWire>,
|
|
|
|
|
},
|
|
|
|
|
ControlUnavailable {
|
|
|
|
|
error: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContextualProcessEventKindWire {
|
|
|
|
|
pub(crate) fn is_terminal(&self) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
self,
|
|
|
|
|
Self::SpawnRejected { .. }
|
|
|
|
|
| Self::SpawnFailed { .. }
|
|
|
|
|
| Self::BootstrapFailed { .. }
|
|
|
|
|
| Self::Exited { .. }
|
|
|
|
|
| Self::ProcessError { .. }
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub(crate) struct ContextualProcessEventWire {
|
|
|
|
|
pub request_id: String,
|
|
|
|
|
pub logical_node_id: u64,
|
|
|
|
|
pub event: ContextualProcessEventKindWire,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) enum ContextualProcessControllerIn {
|
|
|
|
|
Command(ContextualNodeCommand),
|
|
|
|
|
Observed {
|
|
|
|
|
request_id: String,
|
|
|
|
|
output: ContextualProcessOutput,
|
|
|
|
|
},
|
2026-08-28 09:07:31 +00:00
|
|
|
ProgramResolved {
|
|
|
|
|
request_id: String,
|
|
|
|
|
result: Result<BlobBinding, String>,
|
|
|
|
|
},
|
|
|
|
|
ProgramPrepared {
|
|
|
|
|
request_id: String,
|
|
|
|
|
result: Result<PathBuf, String>,
|
|
|
|
|
},
|
2026-08-27 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ContextualOutputRelay {
|
|
|
|
|
request_id: String,
|
|
|
|
|
controller: ActorAddress,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl swactor::actor::ActorInterface for ContextualOutputRelay {
|
|
|
|
|
type Incoming = ContextualProcessOutput;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &swactor::runtime::Ctx<'_>, output: Self::Incoming) {
|
|
|
|
|
let terminal = matches!(
|
|
|
|
|
output,
|
|
|
|
|
ContextualProcessOutput::Process(
|
|
|
|
|
ProcessOutput::SpawnFailed { .. }
|
|
|
|
|
| ProcessOutput::Exited { .. }
|
|
|
|
|
| ProcessOutput::Error { .. }
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
self.controller,
|
|
|
|
|
ContextualProcessControllerIn::Observed {
|
|
|
|
|
request_id: self.request_id.clone(),
|
|
|
|
|
output,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
if terminal {
|
|
|
|
|
ctx.stop_self();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 09:07:31 +00:00
|
|
|
const PROGRAM_TRANSFER_RETRY: Duration = Duration::from_millis(100);
|
|
|
|
|
const PROGRAM_TRANSFER_RETRY_LIMIT: u16 = 300;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct ContextualProgramMaterializer {
|
|
|
|
|
pub namespace_proxy: ActorAddress,
|
|
|
|
|
pub receiver: Arc<dyn BlobTransferReceiver>,
|
|
|
|
|
pub routes: Arc<dyn HostRouteRegistrar>,
|
|
|
|
|
pub engine: EngineHandle,
|
|
|
|
|
pub sender: swactor::runtime::ExternalSender,
|
|
|
|
|
pub root: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct PendingProgramSpawn {
|
|
|
|
|
spec: ContextualProcessSpecWire,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ProgramNamespaceResolver {
|
|
|
|
|
namespace_proxy: ActorAddress,
|
|
|
|
|
source_path: data_plane::path::DataPath,
|
|
|
|
|
request_id: String,
|
|
|
|
|
controller: ActorAddress,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl swactor::actor::ActorInterface for ProgramNamespaceResolver {
|
|
|
|
|
type Incoming = DataDirectoryOut;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn on_start(&mut self, ctx: &swactor::runtime::Ctx<'_>) {
|
|
|
|
|
if ctx
|
|
|
|
|
.send(
|
|
|
|
|
self.namespace_proxy,
|
|
|
|
|
NamespaceClientIn::Request {
|
|
|
|
|
request: NamespaceRequest::Resolve {
|
|
|
|
|
path: self.source_path.clone(),
|
|
|
|
|
},
|
|
|
|
|
reply_to: ctx.self_addr(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
{
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
self.controller,
|
|
|
|
|
ContextualProcessControllerIn::ProgramResolved {
|
|
|
|
|
request_id: self.request_id.clone(),
|
|
|
|
|
result: Err("uploaded program namespace is unavailable".to_owned()),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
ctx.stop_self();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &swactor::runtime::Ctx<'_>, reply: Self::Incoming) {
|
|
|
|
|
let result = match reply {
|
|
|
|
|
DataDirectoryOut::Resolved { result, .. } => {
|
|
|
|
|
result.map_err(|error| format!("resolve uploaded program: {error}"))
|
|
|
|
|
}
|
|
|
|
|
other => Err(format!(
|
|
|
|
|
"resolve uploaded program returned unexpected reply {other:?}"
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
self.controller,
|
|
|
|
|
ContextualProcessControllerIn::ProgramResolved {
|
|
|
|
|
request_id: self.request_id.clone(),
|
|
|
|
|
result,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
ctx.stop_self();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ProgramFileTransfer {
|
|
|
|
|
request_id: String,
|
|
|
|
|
controller: ActorAddress,
|
|
|
|
|
source: ActorAddress,
|
|
|
|
|
length: u64,
|
|
|
|
|
transfer_id: BlobTransferId,
|
|
|
|
|
receiver: Arc<dyn BlobTransferReceiver>,
|
|
|
|
|
routes: Arc<dyn HostRouteRegistrar>,
|
|
|
|
|
engine: EngineHandle,
|
|
|
|
|
sender: swactor::runtime::ExternalSender,
|
|
|
|
|
file: Option<File>,
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
offer: Option<BlobTransferOffer>,
|
|
|
|
|
written: u64,
|
|
|
|
|
source_confirmed: bool,
|
|
|
|
|
route_attempts: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProgramFileTransfer {
|
|
|
|
|
fn fail(&mut self, ctx: &swactor::runtime::Ctx<'_>, error: impl Into<String>) {
|
|
|
|
|
self.finish(ctx, Err(error.into()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn finish(&mut self, ctx: &swactor::runtime::Ctx<'_>, result: Result<PathBuf, String>) {
|
|
|
|
|
if let Some(offer) = self.offer.take() {
|
|
|
|
|
self.receiver.cancel(&offer);
|
|
|
|
|
}
|
|
|
|
|
self.file.take();
|
|
|
|
|
if result.is_err() {
|
|
|
|
|
remove_program_tree(&self.path);
|
|
|
|
|
}
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
self.controller,
|
|
|
|
|
ContextualProcessControllerIn::ProgramPrepared {
|
|
|
|
|
request_id: self.request_id.clone(),
|
|
|
|
|
result,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
ctx.stop_self();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn try_start(&mut self, ctx: &swactor::runtime::Ctx<'_>) {
|
|
|
|
|
if self.route_attempts >= PROGRAM_TRANSFER_RETRY_LIMIT {
|
|
|
|
|
self.fail(
|
|
|
|
|
ctx,
|
|
|
|
|
"uploaded program source did not become routable before the transfer deadline",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.route_attempts += 1;
|
|
|
|
|
if self.routes.is_routable(self.source) {
|
|
|
|
|
let offer = self
|
|
|
|
|
.offer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("program transfer retains its offer")
|
|
|
|
|
.clone();
|
|
|
|
|
if ctx
|
|
|
|
|
.send(self.source, BlobSourceIn::BeginTransfer { offer })
|
|
|
|
|
.is_err()
|
|
|
|
|
{
|
|
|
|
|
self.fail(ctx, "route to uploaded program source is unavailable");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !self.source_confirmed {
|
|
|
|
|
self.engine.send_after(
|
|
|
|
|
PROGRAM_TRANSFER_RETRY,
|
|
|
|
|
self.sender.clone(),
|
|
|
|
|
ctx.self_addr(),
|
|
|
|
|
BlobTransferEvent::RouteRetry,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl swactor::actor::ActorInterface for ProgramFileTransfer {
|
|
|
|
|
type Incoming = BlobTransferEvent;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn on_start(&mut self, ctx: &swactor::runtime::Ctx<'_>) {
|
|
|
|
|
if self.length == 0 {
|
|
|
|
|
self.finish(ctx, Ok(self.path.clone()));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
match self.receiver.open(ctx.self_addr(), self.transfer_id) {
|
|
|
|
|
Ok(offer) => {
|
|
|
|
|
self.offer = Some(offer);
|
|
|
|
|
self.try_start(ctx);
|
|
|
|
|
}
|
|
|
|
|
Err(error) => self.fail(ctx, format!("open uploaded program transfer: {error}")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &swactor::runtime::Ctx<'_>, event: Self::Incoming) {
|
|
|
|
|
match event {
|
|
|
|
|
BlobTransferEvent::RouteRetry if !self.source_confirmed => self.try_start(ctx),
|
|
|
|
|
BlobTransferEvent::Chunk { transfer_id, bytes } if transfer_id == self.transfer_id => {
|
|
|
|
|
self.source_confirmed = true;
|
|
|
|
|
let Some(next) = self
|
|
|
|
|
.written
|
|
|
|
|
.checked_add(bytes.len() as u64)
|
|
|
|
|
.filter(|written| *written <= self.length)
|
|
|
|
|
else {
|
|
|
|
|
self.fail(
|
|
|
|
|
ctx,
|
|
|
|
|
format!("uploaded program exceeded declared length {}", self.length),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
if let Err(error) = self
|
|
|
|
|
.file
|
|
|
|
|
.as_mut()
|
|
|
|
|
.expect("live program transfer owns its file")
|
|
|
|
|
.write_all(&bytes)
|
|
|
|
|
{
|
|
|
|
|
self.fail(ctx, format!("write uploaded program: {error}"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.written = next;
|
|
|
|
|
}
|
|
|
|
|
BlobTransferEvent::Finished { transfer_id } if transfer_id == self.transfer_id => {
|
|
|
|
|
self.source_confirmed = true;
|
|
|
|
|
if self.written != self.length {
|
|
|
|
|
self.fail(
|
|
|
|
|
ctx,
|
|
|
|
|
format!(
|
|
|
|
|
"uploaded program length mismatch: expected {}, received {}",
|
|
|
|
|
self.length, self.written
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if let Err(error) = self
|
|
|
|
|
.file
|
|
|
|
|
.as_mut()
|
|
|
|
|
.expect("live program transfer owns its file")
|
|
|
|
|
.flush()
|
|
|
|
|
{
|
|
|
|
|
self.fail(ctx, format!("flush uploaded program: {error}"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.finish(ctx, Ok(self.path.clone()));
|
|
|
|
|
}
|
|
|
|
|
BlobTransferEvent::Failed {
|
|
|
|
|
transfer_id,
|
|
|
|
|
reason,
|
|
|
|
|
} if transfer_id == self.transfer_id => {
|
|
|
|
|
self.fail(ctx, format!("receive uploaded program: {reason}"));
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn remove_program_tree(path: &Path) {
|
|
|
|
|
if let Some(parent) = path.parent() {
|
|
|
|
|
let _ = fs::remove_dir_all(parent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 22:09:26 +00:00
|
|
|
struct LiveContextualExecution {
|
|
|
|
|
request_id: String,
|
|
|
|
|
process: ActorAddress,
|
|
|
|
|
identity: ExecutionIdentity,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
started_pid: Option<u32>,
|
|
|
|
|
context_ready: bool,
|
2026-08-28 09:07:31 +00:00
|
|
|
staged_program: Option<PathBuf>,
|
2026-08-27 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct ContextualProcessController {
|
|
|
|
|
logical_node_id: u64,
|
|
|
|
|
spawner: Arc<ContextualProcessSpawner>,
|
|
|
|
|
sender: swactor::runtime::ExternalSender,
|
2026-08-28 09:07:31 +00:00
|
|
|
materializer: Option<ContextualProgramMaterializer>,
|
|
|
|
|
pending_programs: HashMap<String, PendingProgramSpawn>,
|
2026-08-27 22:09:26 +00:00
|
|
|
by_process: HashMap<ActorAddress, LiveContextualExecution>,
|
|
|
|
|
process_by_request: HashMap<String, ActorAddress>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContextualProcessController {
|
|
|
|
|
pub(crate) fn new(
|
|
|
|
|
logical_node_id: u64,
|
|
|
|
|
spawner: Arc<ContextualProcessSpawner>,
|
|
|
|
|
sender: swactor::runtime::ExternalSender,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
logical_node_id,
|
|
|
|
|
spawner,
|
|
|
|
|
sender,
|
2026-08-28 09:07:31 +00:00
|
|
|
materializer: None,
|
|
|
|
|
pending_programs: HashMap::new(),
|
2026-08-27 22:09:26 +00:00
|
|
|
by_process: HashMap::new(),
|
|
|
|
|
process_by_request: HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-28 09:07:31 +00:00
|
|
|
pub(crate) fn with_program_materializer(
|
|
|
|
|
mut self,
|
|
|
|
|
materializer: ContextualProgramMaterializer,
|
|
|
|
|
) -> Self {
|
|
|
|
|
self.materializer = Some(materializer);
|
|
|
|
|
self
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
|
|
|
|
|
fn emit(
|
|
|
|
|
&self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
request_id: String,
|
|
|
|
|
event: ContextualProcessEventKindWire,
|
|
|
|
|
) {
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
reply_to,
|
|
|
|
|
crate::orchestration::actor::OrchestratorMsg::ContextualEvent(
|
|
|
|
|
ContextualProcessEventWire {
|
|
|
|
|
request_id,
|
|
|
|
|
logical_node_id: self.logical_node_id,
|
|
|
|
|
event,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn(
|
|
|
|
|
&mut self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
request_id: String,
|
2026-08-28 09:07:31 +00:00
|
|
|
mut spec: ContextualProcessSpecWire,
|
2026-08-27 22:09:26 +00:00
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
) {
|
2026-08-28 09:07:31 +00:00
|
|
|
if self.process_by_request.contains_key(&request_id)
|
|
|
|
|
|| self.pending_programs.contains_key(&request_id)
|
|
|
|
|
{
|
2026-08-27 22:09:26 +00:00
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: "contextual request ID is already live".to_owned(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-08-28 09:07:31 +00:00
|
|
|
let Some(staged_program) = spec.staged_program.take() else {
|
|
|
|
|
self.spawn_ready(ctx, request_id, spec, reply_to, None);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Some(materializer) = self.materializer.clone() else {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: "uploaded program materialization is unavailable on this node"
|
|
|
|
|
.to_owned(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let source_path = match data_plane::path::DataPath::parse(staged_program.namespace_path) {
|
|
|
|
|
Ok(path) => path,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: format!("invalid uploaded program path: {error}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
self.pending_programs
|
|
|
|
|
.insert(request_id.clone(), PendingProgramSpawn { spec, reply_to });
|
|
|
|
|
if let Err(error) = ctx.spawn(ProgramNamespaceResolver {
|
|
|
|
|
namespace_proxy: materializer.namespace_proxy,
|
|
|
|
|
source_path,
|
|
|
|
|
request_id: request_id.clone(),
|
|
|
|
|
controller: ctx.self_addr(),
|
|
|
|
|
}) {
|
|
|
|
|
self.pending_programs.remove(&request_id);
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: format!("spawn uploaded program resolver: {error}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn_ready(
|
|
|
|
|
&mut self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
request_id: String,
|
|
|
|
|
spec: ContextualProcessSpecWire,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
staged_program: Option<PathBuf>,
|
|
|
|
|
) {
|
2026-08-27 22:09:26 +00:00
|
|
|
let spec = match spec.into_spec() {
|
|
|
|
|
Ok(spec) => spec,
|
|
|
|
|
Err(error) => {
|
2026-08-28 09:07:31 +00:00
|
|
|
if let Some(path) = staged_program.as_deref() {
|
|
|
|
|
remove_program_tree(path);
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected { error },
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let relay = match ctx.spawn(ContextualOutputRelay {
|
|
|
|
|
request_id: request_id.clone(),
|
|
|
|
|
controller: ctx.self_addr(),
|
|
|
|
|
}) {
|
|
|
|
|
Ok(relay) => relay,
|
|
|
|
|
Err(error) => {
|
2026-08-28 09:07:31 +00:00
|
|
|
if let Some(path) = staged_program.as_deref() {
|
|
|
|
|
remove_program_tree(path);
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: format!("spawn contextual output relay: {error}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let output = ContextualProcessOutputConfig::disabled(relay);
|
|
|
|
|
match self.spawner.spawn(ctx, &self.sender, spec, output) {
|
|
|
|
|
Ok(spawned) => {
|
|
|
|
|
self.process_by_request
|
|
|
|
|
.insert(request_id.clone(), spawned.actor);
|
|
|
|
|
self.by_process.insert(
|
|
|
|
|
spawned.actor,
|
|
|
|
|
LiveContextualExecution {
|
|
|
|
|
request_id: request_id.clone(),
|
|
|
|
|
process: spawned.actor,
|
|
|
|
|
identity: spawned.identity,
|
|
|
|
|
reply_to,
|
|
|
|
|
started_pid: None,
|
|
|
|
|
context_ready: false,
|
2026-08-28 09:07:31 +00:00
|
|
|
staged_program,
|
2026-08-27 22:09:26 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::Spawned {
|
|
|
|
|
process: spawned.actor,
|
|
|
|
|
identity: spawned.identity.into(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Err(error) => {
|
|
|
|
|
let _ = ctx.stop_actor(relay);
|
2026-08-28 09:07:31 +00:00
|
|
|
if let Some(path) = staged_program.as_deref() {
|
|
|
|
|
remove_program_tree(path);
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: error.to_string(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 09:07:31 +00:00
|
|
|
fn program_resolved(
|
|
|
|
|
&mut self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
request_id: String,
|
|
|
|
|
result: Result<BlobBinding, String>,
|
|
|
|
|
) {
|
|
|
|
|
let Some(pending) = self.pending_programs.get(&request_id) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let reply_to = pending.reply_to;
|
|
|
|
|
let binding = match result {
|
|
|
|
|
Ok(binding) => binding,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
let pending = self
|
|
|
|
|
.pending_programs
|
|
|
|
|
.remove(&request_id)
|
|
|
|
|
.expect("pending uploaded program exists");
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
pending.reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected { error },
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let Some(materializer) = self.materializer.clone() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let path = program_path(&materializer.root, &request_id);
|
|
|
|
|
let file = path
|
|
|
|
|
.parent()
|
|
|
|
|
.ok_or_else(|| "uploaded program path has no parent".to_owned())
|
|
|
|
|
.and_then(|parent| {
|
|
|
|
|
fs::create_dir_all(parent)
|
|
|
|
|
.map_err(|error| format!("create uploaded program directory: {error}"))
|
|
|
|
|
})
|
|
|
|
|
.and_then(|()| {
|
|
|
|
|
File::create(&path)
|
|
|
|
|
.map_err(|error| format!("create uploaded program file: {error}"))
|
|
|
|
|
});
|
|
|
|
|
let file = match file {
|
|
|
|
|
Ok(file) => file,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
self.pending_programs.remove(&request_id);
|
|
|
|
|
remove_program_tree(&path);
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected { error },
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let random = ActorAddress::new_random();
|
|
|
|
|
let transfer_id = BlobTransferId(u64::from_le_bytes(
|
|
|
|
|
random.0[..8]
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("actor address contains eight transfer ID bytes"),
|
|
|
|
|
));
|
|
|
|
|
let transfer = ProgramFileTransfer {
|
|
|
|
|
request_id: request_id.clone(),
|
|
|
|
|
controller: ctx.self_addr(),
|
|
|
|
|
source: binding.source,
|
|
|
|
|
length: binding.length,
|
|
|
|
|
transfer_id,
|
|
|
|
|
receiver: materializer.receiver,
|
|
|
|
|
routes: materializer.routes,
|
|
|
|
|
engine: materializer.engine,
|
|
|
|
|
sender: materializer.sender,
|
|
|
|
|
file: Some(file),
|
|
|
|
|
path: path.clone(),
|
|
|
|
|
offer: None,
|
|
|
|
|
written: 0,
|
|
|
|
|
source_confirmed: false,
|
|
|
|
|
route_attempts: 0,
|
|
|
|
|
};
|
|
|
|
|
if let Err(error) = ctx.spawn(transfer) {
|
|
|
|
|
self.pending_programs.remove(&request_id);
|
|
|
|
|
remove_program_tree(&path);
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected {
|
|
|
|
|
error: format!("spawn uploaded program transfer: {error}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn program_prepared(
|
|
|
|
|
&mut self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
request_id: String,
|
|
|
|
|
result: Result<PathBuf, String>,
|
|
|
|
|
) {
|
|
|
|
|
let Some(mut pending) = self.pending_programs.remove(&request_id) else {
|
|
|
|
|
if let Ok(path) = result {
|
|
|
|
|
remove_program_tree(&path);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let path = match result {
|
|
|
|
|
Ok(path) => path,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
pending.reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::SpawnRejected { error },
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
pending.spec.args.push(path.display().to_string());
|
|
|
|
|
if pending.spec.working_dir.is_none() {
|
|
|
|
|
pending.spec.working_dir = path.parent().map(|parent| parent.display().to_string());
|
|
|
|
|
}
|
|
|
|
|
self.spawn_ready(ctx, request_id, pending.spec, pending.reply_to, Some(path));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 22:09:26 +00:00
|
|
|
fn stop(
|
|
|
|
|
&self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
request_id: String,
|
|
|
|
|
process: ActorAddress,
|
|
|
|
|
kill_after_ms: Option<u64>,
|
|
|
|
|
reply_to: ActorAddress,
|
|
|
|
|
) {
|
|
|
|
|
let event = if self.by_process.contains_key(&process) {
|
|
|
|
|
match send_contextual_process_command(
|
|
|
|
|
&self.sender,
|
|
|
|
|
process,
|
|
|
|
|
ContextualProcessCommand::Stop {
|
|
|
|
|
kill_after: kill_after_ms.map(Duration::from_millis),
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
Ok(()) => ContextualProcessEventKindWire::StopAccepted { process },
|
|
|
|
|
Err(error) => ContextualProcessEventKindWire::StopRejected {
|
|
|
|
|
error: error.to_string(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ContextualProcessEventKindWire::StopRejected {
|
|
|
|
|
error: "contextual process is not live on this node".to_owned(),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
self.emit(ctx, reply_to, request_id, event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn query(&self, ctx: &swactor::runtime::Ctx<'_>, request_id: String, reply_to: ActorAddress) {
|
|
|
|
|
let mut executions = self
|
|
|
|
|
.by_process
|
|
|
|
|
.values()
|
|
|
|
|
.map(|execution| LiveContextualExecutionWire {
|
|
|
|
|
request_id: execution.request_id.clone(),
|
|
|
|
|
process: execution.process,
|
|
|
|
|
identity: execution.identity.into(),
|
|
|
|
|
started_pid: execution.started_pid,
|
|
|
|
|
context_ready: execution.context_ready,
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
executions.sort_by(|left, right| left.request_id.cmp(&right.request_id));
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
reply_to,
|
|
|
|
|
request_id,
|
|
|
|
|
ContextualProcessEventKindWire::LiveExecutions { executions },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn observe(
|
|
|
|
|
&mut self,
|
|
|
|
|
ctx: &swactor::runtime::Ctx<'_>,
|
|
|
|
|
request_id: String,
|
|
|
|
|
output: ContextualProcessOutput,
|
|
|
|
|
) {
|
|
|
|
|
let Some(process) = self.process_by_request.get(&request_id).copied() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let (reply_to, event, terminal) = {
|
|
|
|
|
let Some(execution) = self.by_process.get_mut(&process) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let event = match output {
|
|
|
|
|
ContextualProcessOutput::Process(ProcessOutput::Started { pid }) => {
|
|
|
|
|
execution.started_pid = Some(pid);
|
|
|
|
|
ContextualProcessEventKindWire::ProcessStarted { pid }
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::Process(ProcessOutput::Stdout(bytes)) => {
|
|
|
|
|
ContextualProcessEventKindWire::Stdout { bytes }
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::Process(ProcessOutput::Stderr(bytes)) => {
|
|
|
|
|
ContextualProcessEventKindWire::Stderr { bytes }
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::Process(ProcessOutput::SpawnFailed { error }) => {
|
|
|
|
|
ContextualProcessEventKindWire::SpawnFailed { error }
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::Process(ProcessOutput::Exited { status }) => {
|
|
|
|
|
ContextualProcessEventKindWire::Exited {
|
|
|
|
|
status: status.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::Process(ProcessOutput::Error { error }) => {
|
|
|
|
|
ContextualProcessEventKindWire::ProcessError { error }
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::ContextReady => {
|
|
|
|
|
execution.context_ready = true;
|
|
|
|
|
ContextualProcessEventKindWire::ContextReady
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessOutput::BootstrapFailed { reason } => {
|
|
|
|
|
ContextualProcessEventKindWire::BootstrapFailed {
|
|
|
|
|
error: bootstrap_failure(&reason),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
(execution.reply_to, event.clone(), event.is_terminal())
|
|
|
|
|
};
|
|
|
|
|
self.emit(ctx, reply_to, request_id.clone(), event);
|
|
|
|
|
if terminal {
|
2026-08-28 09:07:31 +00:00
|
|
|
if let Some(execution) = self.by_process.remove(&process)
|
|
|
|
|
&& let Some(path) = execution.staged_program
|
|
|
|
|
{
|
|
|
|
|
remove_program_tree(&path);
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
self.process_by_request.remove(&request_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 09:07:31 +00:00
|
|
|
fn program_path(root: &Path, request_id: &str) -> PathBuf {
|
|
|
|
|
let digest = blake3::hash(request_id.as_bytes()).to_hex();
|
|
|
|
|
root.join(digest.as_str()).join("program.py")
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 22:09:26 +00:00
|
|
|
fn bootstrap_failure(failure: &BootstrapFailure) -> String {
|
|
|
|
|
match failure {
|
|
|
|
|
BootstrapFailure::Provisioning(error)
|
|
|
|
|
| BootstrapFailure::ClaimRejected(error)
|
|
|
|
|
| BootstrapFailure::Attachment(error)
|
|
|
|
|
| BootstrapFailure::ProcessErrorBeforeReady(error)
|
|
|
|
|
| BootstrapFailure::SessionFault(error) => error.clone(),
|
|
|
|
|
BootstrapFailure::StopRequested => "stop requested".to_owned(),
|
|
|
|
|
BootstrapFailure::ChannelClosed => "bootstrap channel closed".to_owned(),
|
|
|
|
|
BootstrapFailure::AttachmentDeadline => "attachment deadline exceeded".to_owned(),
|
|
|
|
|
BootstrapFailure::ProcessExitedBeforeReady => {
|
|
|
|
|
"process exited before context ready".to_owned()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl swactor::actor::ActorInterface for ContextualProcessController {
|
|
|
|
|
type Incoming = ContextualProcessControllerIn;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &swactor::runtime::Ctx<'_>, message: Self::Incoming) {
|
|
|
|
|
match message {
|
|
|
|
|
ContextualProcessControllerIn::Command(ContextualNodeCommand::Spawn {
|
|
|
|
|
request_id,
|
|
|
|
|
spec,
|
|
|
|
|
reply_to,
|
|
|
|
|
}) => self.spawn(ctx, request_id, spec, reply_to),
|
|
|
|
|
ContextualProcessControllerIn::Command(ContextualNodeCommand::Stop {
|
|
|
|
|
request_id,
|
|
|
|
|
process,
|
|
|
|
|
kill_after_ms,
|
|
|
|
|
reply_to,
|
|
|
|
|
}) => self.stop(ctx, request_id, process, kill_after_ms, reply_to),
|
|
|
|
|
ContextualProcessControllerIn::Command(ContextualNodeCommand::Query {
|
|
|
|
|
request_id,
|
|
|
|
|
reply_to,
|
|
|
|
|
}) => self.query(ctx, request_id, reply_to),
|
|
|
|
|
ContextualProcessControllerIn::Observed { request_id, output } => {
|
|
|
|
|
self.observe(ctx, request_id, output);
|
|
|
|
|
}
|
2026-08-28 09:07:31 +00:00
|
|
|
ContextualProcessControllerIn::ProgramResolved { request_id, result } => {
|
|
|
|
|
self.program_resolved(ctx, request_id, result);
|
|
|
|
|
}
|
|
|
|
|
ContextualProcessControllerIn::ProgramPrepared { request_id, result } => {
|
|
|
|
|
self.program_prepared(ctx, request_id, result);
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|