2026-08-17 22:51:08 +00:00
|
|
|
//! Node-side job actor — the spec §5/§7 executor. It receives orchestrator
|
|
|
|
|
//! commands over the actor plane and runs `setup`/`run` as supervised processes
|
|
|
|
|
//! via `swactor-process`. Bulk bytes (workspace push / output pull) travel either
|
|
|
|
|
//! as chunked actor messages (the in-process test path) or over the EDGE_ALPN
|
|
|
|
|
//! byte transport driven by the integration layer (the real iroh path); which
|
|
|
|
|
//! path is used is decided by which optional edge capabilities the constructor is
|
|
|
|
|
//! given. Lifecycle stays on the actor plane either way.
|
|
|
|
|
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
use parking_lot::Mutex;
|
2026-08-17 22:51:08 +00:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::sync::Arc;
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
2026-08-17 22:51:08 +00:00
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
use crate::orchestrator::OrchestratorJobMsg;
|
|
|
|
|
use crate::wire::{
|
|
|
|
|
CHUNK_SIZE, EDGE_RECORD_SIZE, JobEdgeSink, NodeJobCommand, NodeJobEvent, OutputChunk,
|
|
|
|
|
};
|
2026-08-17 22:51:08 +00:00
|
|
|
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
|
|
|
|
|
use swactor::runtime::ExternalSender;
|
|
|
|
|
use swactor_process::{
|
|
|
|
|
ExitStatus, ProcessOutput, ProcessOutputConfig, ProcessSpec, spawn_local_process,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// Which supervised phase a process exit belongs to.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
pub enum JobPhase {
|
|
|
|
|
Setup,
|
|
|
|
|
Run,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The node-side executor. `Incoming` is the orchestrator↔node wire command.
|
|
|
|
|
pub struct NodeJobActor {
|
|
|
|
|
orchestrator: ActorAddress,
|
|
|
|
|
workdir: PathBuf,
|
|
|
|
|
sender: ExternalSender,
|
|
|
|
|
job_id: u64,
|
|
|
|
|
workspace_buf: Vec<u8>,
|
|
|
|
|
/// Edge-mode workspace-ready flag. When set, the orchestrator pushed the
|
|
|
|
|
/// workspace tar over EDGE_ALPN (drained + extracted by the integration
|
|
|
|
|
/// layer); `MaterializeWorkspace` waits for it before emitting
|
|
|
|
|
/// `WorkspaceMaterialized`. `None` ⇒ in-process chunk path.
|
|
|
|
|
workspace_ready: Option<Arc<AtomicBool>>,
|
|
|
|
|
/// Edge-mode output sink slot, filled by the integration layer once the iroh
|
|
|
|
|
/// connection to the orchestrator is up. `None` ⇒ chunk path (outputs stream
|
|
|
|
|
/// back as `OutputChunk` actor messages).
|
|
|
|
|
output_sink: Option<Arc<Mutex<Option<Box<dyn JobEdgeSink>>>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// How long the node waits for an edge-mode workspace transfer to land before
|
|
|
|
|
/// faulting. Generous because the workspace is pushed over a relay.
|
|
|
|
|
const WORKSPACE_EDGE_WAIT: Duration = Duration::from_secs(60 * 10);
|
|
|
|
|
/// How long the node waits for the integration layer to arm the output edge sink.
|
|
|
|
|
const OUTPUT_EDGE_ARM_WAIT: Duration = Duration::from_secs(60 * 5);
|
|
|
|
|
/// Poll interval for edge-mode spin-waits inside the actor.
|
|
|
|
|
const EDGE_SPIN: Duration = Duration::from_millis(25);
|
|
|
|
|
|
|
|
|
|
impl NodeJobActor {
|
|
|
|
|
pub fn new(
|
|
|
|
|
orchestrator: ActorAddress,
|
|
|
|
|
workdir: PathBuf,
|
|
|
|
|
sender: ExternalSender,
|
|
|
|
|
job_id: u64,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
orchestrator,
|
|
|
|
|
workdir,
|
|
|
|
|
sender,
|
|
|
|
|
job_id,
|
|
|
|
|
workspace_buf: Vec::new(),
|
|
|
|
|
workspace_ready: None,
|
|
|
|
|
output_sink: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Edge mode: workspace bytes arrive over EDGE_ALPN and are extracted by the
|
|
|
|
|
/// integration layer, which sets `flag` once the workspace is on disk.
|
|
|
|
|
pub fn with_workspace_ready(mut self, flag: Arc<AtomicBool>) -> Self {
|
|
|
|
|
self.workspace_ready = Some(flag);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Edge mode: ship collected outputs over EDGE_ALPN through `slot`, which the
|
|
|
|
|
/// integration layer fills with a byte sink once the connection is up.
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
pub fn with_output_sink_slot(mut self, slot: Arc<Mutex<Option<Box<dyn JobEdgeSink>>>>) -> Self {
|
2026-08-17 22:51:08 +00:00
|
|
|
self.output_sink = Some(slot);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn emit(&self, ctx: &Ctx, event: NodeJobEvent) {
|
|
|
|
|
let _ = ctx.send(self.orchestrator, OrchestratorJobMsg::NodeEvent(event));
|
|
|
|
|
}
|
|
|
|
|
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
fn spawn_supervised(
|
|
|
|
|
&self,
|
|
|
|
|
ctx: &Ctx,
|
|
|
|
|
phase: JobPhase,
|
|
|
|
|
command: String,
|
|
|
|
|
env: &BTreeMap<String, String>,
|
|
|
|
|
) {
|
2026-08-17 22:51:08 +00:00
|
|
|
let relay = match ctx.spawn(ProcessExitRelay {
|
|
|
|
|
orchestrator: self.orchestrator,
|
|
|
|
|
phase,
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
}) {
|
|
|
|
|
Ok(addr) => addr,
|
|
|
|
|
Err(e) => {
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
reason: format!("spawn relay: {e}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-08-17 22:51:08 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let spec = ProcessSpec {
|
|
|
|
|
command: "bash".to_owned(),
|
|
|
|
|
args: vec!["-c".to_owned(), command],
|
|
|
|
|
env: env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
|
|
|
|
|
working_dir: Some(self.workdir.clone()),
|
|
|
|
|
label: Some(format!("job-runner-{:?}", phase).to_lowercase()),
|
|
|
|
|
};
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
if let Err(e) = spawn_local_process(
|
|
|
|
|
ctx,
|
|
|
|
|
&self.sender,
|
|
|
|
|
spec,
|
|
|
|
|
ProcessOutputConfig::disabled(relay),
|
|
|
|
|
) {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
reason: format!("spawn process: {e}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-08-17 22:51:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Edge-mode output collection: pack every declared output into a single tar
|
|
|
|
|
/// and ship it over EDGE_ALPN, then announce collection. Dropping the sink
|
|
|
|
|
/// finishes the edge stream so the orchestrator observes end-of-stream.
|
|
|
|
|
fn collect_outputs_edge(
|
|
|
|
|
&self,
|
|
|
|
|
ctx: &Ctx,
|
|
|
|
|
job_id: u64,
|
|
|
|
|
outputs: &[String],
|
|
|
|
|
slot: Arc<Mutex<Option<Box<dyn JobEdgeSink>>>>,
|
|
|
|
|
) {
|
|
|
|
|
let sink = match self.take_output_sink(ctx, job_id, &slot) {
|
|
|
|
|
Some(sink) => sink,
|
|
|
|
|
None => return, // already faulted while waiting for the sink
|
|
|
|
|
};
|
|
|
|
|
let bytes = match pack_outputs_tar(&self.workdir, outputs) {
|
|
|
|
|
Ok(b) => b,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id,
|
|
|
|
|
reason: format!("pack outputs: {e}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
// Finish the stream + announce so the lifecycle FSM can terminate.
|
|
|
|
|
let _ = sink.send_bytes(Vec::new());
|
|
|
|
|
drop(sink);
|
|
|
|
|
self.emit(ctx, NodeJobEvent::OutputsCollected { job_id });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let mut send_err: Option<String> = None;
|
|
|
|
|
for record in bytes.chunks(EDGE_RECORD_SIZE) {
|
|
|
|
|
if let Err(e) = sink.send_bytes(record.to_vec()) {
|
|
|
|
|
send_err = Some(e);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(e) = send_err {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id,
|
|
|
|
|
reason: format!("edge output send: {e}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
drop(sink);
|
|
|
|
|
self.emit(ctx, NodeJobEvent::OutputsCollected { job_id });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Take the edge output sink from the shared slot, waiting briefly for the
|
|
|
|
|
/// integration layer to arm it. Emits a fault and returns `None` on timeout.
|
|
|
|
|
fn take_output_sink(
|
|
|
|
|
&self,
|
|
|
|
|
ctx: &Ctx,
|
|
|
|
|
job_id: u64,
|
|
|
|
|
slot: &Arc<Mutex<Option<Box<dyn JobEdgeSink>>>>,
|
|
|
|
|
) -> Option<Box<dyn JobEdgeSink>> {
|
|
|
|
|
let deadline = Instant::now() + OUTPUT_EDGE_ARM_WAIT;
|
|
|
|
|
loop {
|
|
|
|
|
{
|
|
|
|
|
let mut guard = slot.lock();
|
|
|
|
|
if guard.is_some() {
|
|
|
|
|
return guard.take();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if Instant::now() >= deadline {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id,
|
|
|
|
|
reason: "output edge sink was never armed".to_owned(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(EDGE_SPIN);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActorInterface for NodeJobActor {
|
|
|
|
|
type Incoming = NodeJobCommand;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &Ctx, cmd: NodeJobCommand) {
|
|
|
|
|
match cmd {
|
|
|
|
|
NodeJobCommand::MaterializeWorkspace { job_id } => {
|
|
|
|
|
if let Some(flag) = self.workspace_ready.as_ref() {
|
|
|
|
|
// Edge mode: the workspace tar traveled over EDGE_ALPN and
|
|
|
|
|
// was extracted into `workdir` by the integration layer.
|
|
|
|
|
// Wait for its readiness signal before announcing ready.
|
|
|
|
|
let deadline = Instant::now() + WORKSPACE_EDGE_WAIT;
|
|
|
|
|
while !flag.load(Ordering::Acquire) {
|
|
|
|
|
if Instant::now() >= deadline {
|
|
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id,
|
|
|
|
|
reason: "workspace edge transfer did not land".to_owned(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(EDGE_SPIN);
|
|
|
|
|
}
|
|
|
|
|
self.emit(ctx, NodeJobEvent::WorkspaceMaterialized { job_id });
|
|
|
|
|
} else {
|
|
|
|
|
// Chunk mode: clear the buffer; bytes arrive as WorkspaceChunk.
|
|
|
|
|
self.workspace_buf.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
NodeJobCommand::WorkspaceChunk {
|
|
|
|
|
job_id, data, eof, ..
|
|
|
|
|
} => {
|
2026-08-17 22:51:08 +00:00
|
|
|
self.workspace_buf.extend_from_slice(&data);
|
|
|
|
|
if eof {
|
|
|
|
|
let buf = std::mem::take(&mut self.workspace_buf);
|
|
|
|
|
match extract_tar(&buf, &self.workdir) {
|
|
|
|
|
Ok(()) => self.emit(ctx, NodeJobEvent::WorkspaceMaterialized { job_id }),
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
Err(e) => self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id,
|
|
|
|
|
reason: format!("untar workspace: {e}"),
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-08-17 22:51:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
NodeJobCommand::RunSetup { command, env, .. } => {
|
|
|
|
|
self.spawn_supervised(ctx, JobPhase::Setup, command, &env);
|
|
|
|
|
}
|
|
|
|
|
NodeJobCommand::RunJob { command, env, .. } => {
|
|
|
|
|
self.spawn_supervised(ctx, JobPhase::Run, command, &env);
|
|
|
|
|
}
|
|
|
|
|
NodeJobCommand::CollectOutputs { job_id, outputs } => {
|
|
|
|
|
if let Some(slot) = self.output_sink.clone() {
|
|
|
|
|
// Edge mode: pack all outputs into one tar and ship over
|
|
|
|
|
// EDGE_ALPN, then announce collection. Dropping the sink
|
|
|
|
|
// finishes the stream so the orchestrator sees end-of-stream.
|
|
|
|
|
self.collect_outputs_edge(ctx, job_id, &outputs, slot);
|
|
|
|
|
} else {
|
|
|
|
|
for name in &outputs {
|
|
|
|
|
let path = self.workdir.join(name);
|
|
|
|
|
let bytes = match pack_path(&path) {
|
|
|
|
|
Ok(b) => b,
|
|
|
|
|
Err(e) => {
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
self.emit(
|
|
|
|
|
ctx,
|
|
|
|
|
NodeJobEvent::NodeFault {
|
|
|
|
|
job_id,
|
|
|
|
|
reason: format!("pack output {name}: {e}"),
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-08-17 22:51:08 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
stream_chunks(ctx, self.orchestrator, self.job_id, name, &bytes);
|
|
|
|
|
}
|
|
|
|
|
self.emit(ctx, NodeJobEvent::OutputsCollected { job_id });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Relays `swactor-process` exits to the orchestrator as lifecycle events.
|
|
|
|
|
pub struct ProcessExitRelay {
|
|
|
|
|
pub orchestrator: ActorAddress,
|
|
|
|
|
pub phase: JobPhase,
|
|
|
|
|
pub job_id: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActorInterface for ProcessExitRelay {
|
|
|
|
|
type Incoming = ProcessOutput;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &Ctx, output: ProcessOutput) {
|
|
|
|
|
if let ProcessOutput::Exited { status } = output {
|
|
|
|
|
let event = match (self.phase, status) {
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
(JobPhase::Setup, ExitStatus::Code(0)) => NodeJobEvent::SetupCompleted {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
},
|
2026-08-17 22:51:08 +00:00
|
|
|
(JobPhase::Setup, ExitStatus::Code(c)) => NodeJobEvent::NodeFault {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
reason: format!("setup exited {c}"),
|
|
|
|
|
},
|
|
|
|
|
(JobPhase::Setup, _) => NodeJobEvent::NodeFault {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
reason: "setup exited without a code".to_owned(),
|
|
|
|
|
},
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
(JobPhase::Run, ExitStatus::Code(c)) => NodeJobEvent::JobExited {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
code: c,
|
|
|
|
|
},
|
|
|
|
|
(JobPhase::Run, _) => NodeJobEvent::JobExited {
|
|
|
|
|
job_id: self.job_id,
|
|
|
|
|
code: 1,
|
|
|
|
|
},
|
2026-08-17 22:51:08 +00:00
|
|
|
};
|
|
|
|
|
let _ = ctx.send(self.orchestrator, OrchestratorJobMsg::NodeEvent(event));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn extract_tar(bytes: &[u8], dst: &std::path::Path) -> Result<(), String> {
|
|
|
|
|
std::fs::create_dir_all(dst).map_err(|e| e.to_string())?;
|
|
|
|
|
let mut archive = tar::Archive::new(Cursor::new(bytes));
|
|
|
|
|
archive.unpack(dst).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn pack_path(path: &std::path::Path) -> Result<Vec<u8>, String> {
|
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
|
let mut builder = tar::Builder::new(&mut buf);
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
let name = path
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(|n| n.to_string_lossy().to_string())
|
|
|
|
|
.unwrap_or_else(|| "output".into());
|
2026-08-17 22:51:08 +00:00
|
|
|
if path.is_dir() {
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
builder
|
|
|
|
|
.append_dir_all(&name, path)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
2026-08-17 22:51:08 +00:00
|
|
|
} else if path.is_file() {
|
|
|
|
|
let mut f = std::fs::File::open(path).map_err(|e| e.to_string())?;
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
builder
|
|
|
|
|
.append_file(&name, &mut f)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
2026-08-17 22:51:08 +00:00
|
|
|
} else {
|
|
|
|
|
// Missing outputs are skipped (best-effort): emit an empty eof chunk.
|
|
|
|
|
}
|
|
|
|
|
builder.finish().map_err(|e| e.to_string())?;
|
|
|
|
|
drop(builder);
|
|
|
|
|
Ok(buf)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pack every declared output (relative to `workdir`) into a single tar, each
|
|
|
|
|
/// entry named by its declared output path. Missing outputs are skipped
|
|
|
|
|
/// (best-effort). Used by the edge-mode output path to ship all outputs in one
|
|
|
|
|
/// EDGE_ALPN stream.
|
|
|
|
|
fn pack_outputs_tar(workdir: &Path, outputs: &[String]) -> Result<Vec<u8>, String> {
|
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
|
let mut builder = tar::Builder::new(&mut buf);
|
|
|
|
|
for name in outputs {
|
|
|
|
|
let path = workdir.join(name);
|
|
|
|
|
if path.is_dir() {
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
builder
|
|
|
|
|
.append_dir_all(name, &path)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
2026-08-17 22:51:08 +00:00
|
|
|
} else if path.is_file() {
|
|
|
|
|
let mut f = std::fs::File::open(&path).map_err(|e| e.to_string())?;
|
Rework Myelin control and runtime integration
Add actor-backed manual node provisioning, control-plane endpoints, and fleet UI assets with durable provider lifecycle handling.
Simplify Myelin orchestration, node runtime, staging, and telemetry paths while removing obsolete engine-builder, dashboard-view, and local-mock implementations.
Align runtime delivery, data-plane, distribution, job-runner, process, telemetry, dashboard, Vast.ai integrations, and their tests with the revised actor and transport contracts.
2026-08-19 10:20:01 +00:00
|
|
|
builder
|
|
|
|
|
.append_file(name, &mut f)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
2026-08-17 22:51:08 +00:00
|
|
|
}
|
|
|
|
|
// Missing outputs are skipped (best-effort).
|
|
|
|
|
}
|
|
|
|
|
builder.finish().map_err(|e| e.to_string())?;
|
|
|
|
|
drop(builder);
|
|
|
|
|
Ok(buf)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stream_chunks(ctx: &Ctx, orchestrator: ActorAddress, job_id: u64, name: &str, bytes: &[u8]) {
|
|
|
|
|
if bytes.is_empty() {
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
orchestrator,
|
|
|
|
|
OrchestratorJobMsg::OutputChunk(OutputChunk {
|
|
|
|
|
job_id,
|
|
|
|
|
name: name.to_owned(),
|
|
|
|
|
seq: 0,
|
|
|
|
|
data: Vec::new(),
|
|
|
|
|
eof: true,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let chunks: Vec<&[u8]> = bytes.chunks(CHUNK_SIZE).collect();
|
|
|
|
|
let total = chunks.len() as u64;
|
|
|
|
|
for (i, chunk) in chunks.iter().enumerate() {
|
|
|
|
|
let _ = ctx.send(
|
|
|
|
|
orchestrator,
|
|
|
|
|
OrchestratorJobMsg::OutputChunk(OutputChunk {
|
|
|
|
|
job_id,
|
|
|
|
|
name: name.to_owned(),
|
|
|
|
|
seq: i as u64,
|
|
|
|
|
data: chunk.to_vec(),
|
|
|
|
|
eof: i as u64 + 1 == total,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|