Skip to main content

lenso_runner/
replicated.rs

1use std::{
2    collections::BTreeMap,
3    fmt,
4    sync::{
5        Arc,
6        atomic::{AtomicBool, Ordering},
7        mpsc as std_mpsc,
8    },
9    thread,
10    time::{Duration, Instant},
11};
12
13use cpu_time::ThreadTime;
14use futures::{channel::oneshot, future::LocalBoxFuture};
15use lenso_app_plan::{CapabilityBinding, ExecutionLaneId, ResolvedAppPlan};
16use lenso_kernel::{
17    CancellationToken, DiagnosticEvent, DiagnosticFilter, DiagnosticSource,
18    ExecutionAdapterCatalog, NativeApp, RequestCapability, RuntimeDiagnostics, RuntimeFailure,
19    ShutdownOutcome,
20};
21use tokio::sync::mpsc;
22
23use crate::TokioDriver;
24
25mod diagnostics;
26mod projection;
27mod transfer;
28
29pub use diagnostics::LaneDiagnosticsSnapshot;
30use diagnostics::LaneDiagnosticsState;
31use projection::{LaneProxyAdapter, project_lane};
32pub use transfer::CrossLaneRequestCatalog;
33
34const LANE_PROXY_EXECUTION_CLASS: &str = "lenso.native-lane-proxy@1";
35
36/// Placement-independent controls applied by the provider lane's Runtime Driver.
37#[derive(Clone, Debug, Default)]
38pub struct LaneInvocationOptions {
39    timeout: Option<Duration>,
40    cancellation: Option<LaneCancellationToken>,
41}
42
43impl LaneInvocationOptions {
44    /// Creates an invocation without a deadline.
45    pub const fn new() -> Self {
46        Self {
47            timeout: None,
48            cancellation: None,
49        }
50    }
51
52    /// Applies a provider-Driver-relative deadline.
53    #[must_use]
54    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
55        self.timeout = Some(timeout);
56        self
57    }
58
59    /// Propagates one caller-owned cooperative cancellation signal.
60    #[must_use]
61    pub fn with_cancellation(mut self, cancellation: LaneCancellationToken) -> Self {
62        self.cancellation = Some(cancellation);
63        self
64    }
65}
66
67/// A thread-safe caller signal translated to a lane-local Kernel cancellation token.
68#[derive(Clone, Debug, Default)]
69pub struct LaneCancellationToken {
70    cancelled: Arc<AtomicBool>,
71}
72
73impl LaneCancellationToken {
74    /// Creates a signal that has not been cancelled.
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Requests cooperative cancellation of attached invocations.
80    pub fn cancel(&self) {
81        self.cancelled.store(true, Ordering::Release);
82    }
83
84    /// Returns whether cancellation was requested.
85    pub fn is_cancelled(&self) -> bool {
86        self.cancelled.load(Ordering::Acquire)
87    }
88}
89
90type LaneTask = Box<dyn FnOnce(NativeApp) -> LocalBoxFuture<'static, ()> + Send + 'static>;
91type LaneSender = mpsc::Sender<LaneCommand>;
92type LaneRoute = mpsc::WeakSender<LaneCommand>;
93
94enum LaneCommand {
95    Run(LaneTask),
96    Shutdown {
97        timeout: Duration,
98        completed: oneshot::Sender<ShutdownOutcome>,
99    },
100}
101
102struct LaneHandle {
103    id: ExecutionLaneId,
104    commands: LaneSender,
105    thread: thread::JoinHandle<()>,
106}
107
108/// A native Runner startup or lane-lifecycle failure.
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub enum ReplicatedRunnerError {
111    /// The immutable Plan failed validation before any lane started.
112    InvalidPlan { detail: String },
113    /// One lane could not construct or start its Kernel replica.
114    LaneStartup { lane: String, detail: String },
115    /// A generated request Capability was not registered with the native transfer catalog.
116    MissingCrossLaneRequestTransfer { capability: String },
117    /// A lane stopped accepting Runner commands unexpectedly.
118    LaneUnavailable { lane: String },
119    /// A lane thread panicked while stopping.
120    LanePanicked { lane: String },
121    /// One Kernel replica did not stop cleanly.
122    LaneShutdown {
123        lane: String,
124        outcome: ShutdownOutcome,
125    },
126}
127
128impl fmt::Display for ReplicatedRunnerError {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::InvalidPlan { detail } => {
132                write!(formatter, "invalid Resolved App Plan: {detail}")
133            }
134            Self::LaneStartup { lane, detail } => {
135                write!(
136                    formatter,
137                    "Execution Lane `{lane}` failed to start: {detail}"
138                )
139            }
140            Self::MissingCrossLaneRequestTransfer { capability } => write!(
141                formatter,
142                "Capability `{capability}` has no registered native cross-lane request transfer"
143            ),
144            Self::LaneUnavailable { lane } => {
145                write!(formatter, "Execution Lane `{lane}` is unavailable")
146            }
147            Self::LanePanicked { lane } => write!(formatter, "Execution Lane `{lane}` panicked"),
148            Self::LaneShutdown { lane, outcome } => write!(
149                formatter,
150                "Execution Lane `{lane}` stopped with {outcome:?}"
151            ),
152        }
153    }
154}
155
156impl std::error::Error for ReplicatedRunnerError {}
157
158/// One native App executed as a fixed set of Plan-declared single-owner Kernel lanes.
159pub struct ReplicatedNativeApp {
160    plan: Arc<ResolvedAppPlan>,
161    lanes: BTreeMap<ExecutionLaneId, LaneHandle>,
162    diagnostics: Arc<LaneDiagnosticsState>,
163    epoch: Instant,
164}
165
166impl fmt::Debug for ReplicatedNativeApp {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter
169            .debug_struct("ReplicatedNativeApp")
170            .field("lanes", &self.lanes.keys().collect::<Vec<_>>())
171            .finish_non_exhaustive()
172    }
173}
174
175impl ReplicatedNativeApp {
176    /// Starts one unmodified Kernel replica per declared Execution Lane.
177    pub fn start<F>(plan: ResolvedAppPlan, adapters: F) -> Result<Self, ReplicatedRunnerError>
178    where
179        F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
180    {
181        Self::start_with_transfers(plan, adapters, CrossLaneRequestCatalog::new())
182    }
183
184    /// Starts replicated lanes with generated request types allowed to cross them.
185    pub fn start_with_transfers<F>(
186        plan: ResolvedAppPlan,
187        adapters: F,
188        transfers: CrossLaneRequestCatalog,
189    ) -> Result<Self, ReplicatedRunnerError>
190    where
191        F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
192    {
193        plan.validate()
194            .map_err(|error| ReplicatedRunnerError::InvalidPlan {
195                detail: error.to_string(),
196            })?;
197        transfers.validate_plan(&plan)?;
198        let plan = Arc::new(plan);
199        let adapters = Arc::new(adapters);
200        let diagnostics = Arc::new(LaneDiagnosticsState::new(Arc::clone(&plan)));
201        let epoch = Instant::now();
202        let mut receivers = BTreeMap::new();
203        let senders = plan
204            .execution_lanes()
205            .iter()
206            .map(|lane| {
207                let (sender, receiver) = mpsc::channel(64);
208                receivers.insert(lane.id().clone(), receiver);
209                (lane.id().clone(), sender)
210            })
211            .collect::<BTreeMap<_, _>>();
212        let routes = Arc::new(
213            senders
214                .iter()
215                .map(|(lane, sender)| (lane.clone(), sender.downgrade()))
216                .collect::<BTreeMap<_, _>>(),
217        );
218        let projected = plan
219            .execution_lanes()
220            .iter()
221            .map(|lane| {
222                project_lane(&plan, lane.id()).map(|projected| (lane.id().clone(), projected))
223            })
224            .collect::<Result<Vec<_>, _>>()?;
225        let mut lanes = BTreeMap::new();
226        let mut startups = Vec::new();
227
228        for (lane_id, lane_plan) in projected {
229            let commands = senders
230                .get(&lane_id)
231                .expect("every declared lane has a command route")
232                .clone();
233            let receiver = receivers
234                .remove(&lane_id)
235                .expect("every declared lane has one command receiver");
236            let (started, startup) = std_mpsc::sync_channel(1);
237            let lane_adapters = Arc::clone(&adapters);
238            let lane_diagnostics = Arc::clone(&diagnostics);
239            let proxy_adapter = LaneProxyAdapter::new(
240                Arc::clone(&plan),
241                transfers.clone(),
242                Arc::clone(&routes),
243                epoch,
244            );
245            let thread_lane = lane_id.clone();
246            let lane_thread = match thread::Builder::new()
247                .name(format!("lenso-lane-{}", lane_id.as_str()))
248                .spawn(move || {
249                    run_lane(
250                        thread_lane,
251                        lane_plan,
252                        receiver,
253                        started,
254                        lane_adapters,
255                        proxy_adapter,
256                        lane_diagnostics,
257                        epoch,
258                    );
259                }) {
260                Ok(thread) => thread,
261                Err(error) => {
262                    drop(receivers);
263                    drop(routes);
264                    drop(senders);
265                    stop_lanes(lanes);
266                    return Err(ReplicatedRunnerError::LaneStartup {
267                        lane: lane_id.to_string(),
268                        detail: error.to_string(),
269                    });
270                }
271            };
272            startups.push((lane_id.clone(), startup));
273            lanes.insert(
274                lane_id.clone(),
275                LaneHandle {
276                    id: lane_id,
277                    commands,
278                    thread: lane_thread,
279                },
280            );
281        }
282
283        for (lane, startup) in startups {
284            match startup.recv() {
285                Ok(Ok(())) => {}
286                Ok(Err(detail)) => {
287                    drop(routes);
288                    drop(senders);
289                    stop_lanes(lanes);
290                    return Err(ReplicatedRunnerError::LaneStartup {
291                        lane: lane.to_string(),
292                        detail,
293                    });
294                }
295                Err(_) => {
296                    drop(routes);
297                    drop(senders);
298                    stop_lanes(lanes);
299                    return Err(ReplicatedRunnerError::LaneUnavailable {
300                        lane: lane.to_string(),
301                    });
302                }
303            }
304        }
305
306        Ok(Self {
307            plan,
308            lanes,
309            diagnostics,
310            epoch,
311        })
312    }
313
314    /// Returns the fixed number of Kernel replicas started from the Plan.
315    pub fn lane_count(&self) -> usize {
316        self.lanes.len()
317    }
318
319    /// Returns structural evidence for placement decisions without exposing payloads.
320    pub fn diagnostics_snapshot(&self) -> LaneDiagnosticsSnapshot {
321        self.diagnostics.snapshot()
322    }
323
324    /// Invokes one generated request Capability on its Plan-placed provider lane.
325    pub async fn invoke<C: RequestCapability>(
326        &self,
327        caller_instance: &str,
328        operation: &str,
329        request: C::Request,
330    ) -> Result<Result<C::Response, C::DomainError>, RuntimeFailure>
331    where
332        C::Request: Send,
333        C::Response: Send,
334        C::DomainError: Send,
335    {
336        self.invoke_with_options::<C>(
337            caller_instance,
338            operation,
339            request,
340            LaneInvocationOptions::new(),
341        )
342        .await
343    }
344
345    /// Invokes one generated request with Driver-relative controls on its provider lane.
346    pub async fn invoke_with_options<C: RequestCapability>(
347        &self,
348        caller_instance: &str,
349        operation: &str,
350        request: C::Request,
351        options: LaneInvocationOptions,
352    ) -> Result<Result<C::Response, C::DomainError>, RuntimeFailure>
353    where
354        C::Request: Send,
355        C::Response: Send,
356        C::DomainError: Send,
357    {
358        let _ = singular_binding::<C>(&self.plan, caller_instance)?;
359        let consumer = self.plan.module_instance(caller_instance).ok_or_else(|| {
360            RuntimeFailure::InvalidResolvedPlan {
361                detail: format!("binding consumer `{caller_instance}` is absent from the Plan"),
362            }
363        })?;
364        let lane =
365            self.lanes
366                .get(consumer.execution_lane())
367                .ok_or_else(|| RuntimeFailure::Internal {
368                    detail: format!(
369                        "Execution Lane `{}` is unavailable",
370                        consumer.execution_lane()
371                    ),
372                })?;
373        let caller_instance = caller_instance.to_owned();
374        let operation = operation.to_owned();
375        let deadline = options
376            .timeout
377            .map(|timeout| self.epoch.elapsed().saturating_add(timeout));
378        let (completed, completion) = oneshot::channel();
379        lane.commands
380            .send(LaneCommand::Run(Box::new(move |app| {
381                Box::pin(async move {
382                    let cancellation = CancellationToken::new();
383                    let completed_signal = Arc::new(AtomicBool::new(false));
384                    if let Some(external) = options.cancellation.clone() {
385                        let local = cancellation.clone();
386                        let watcher_completed = Arc::clone(&completed_signal);
387                        tokio::task::spawn_local(async move {
388                            while !external.is_cancelled()
389                                && !watcher_completed.load(Ordering::Acquire)
390                            {
391                                tokio::task::yield_now().await;
392                            }
393                            if external.is_cancelled() {
394                                local.cancel();
395                            }
396                        });
397                    }
398                    let result = if deadline.is_some() || options.cancellation.is_some() {
399                        let context = app.invocation_context(deadline, cancellation);
400                        app.invoke_with_context::<C>(&caller_instance, &operation, context, request)
401                            .await
402                    } else {
403                        app.invoke::<C>(&caller_instance, &operation, request).await
404                    };
405                    completed_signal.store(true, Ordering::Release);
406                    let _ = completed.send(result);
407                })
408            })))
409            .await
410            .map_err(|_| RuntimeFailure::Internal {
411                detail: format!("Execution Lane `{}` is unavailable", lane.id),
412            })?;
413        completion.await.map_err(|_| RuntimeFailure::Internal {
414            detail: format!("Execution Lane `{}` dropped an invocation", lane.id),
415        })?
416    }
417
418    /// Stops every Kernel replica with the same bounded shutdown timeout.
419    pub async fn shutdown(self, timeout: Duration) -> Result<(), ReplicatedRunnerError> {
420        let mut completions = Vec::new();
421        let mut threads = Vec::new();
422        let mut first_error = None;
423        for (_, lane) in self.lanes {
424            let LaneHandle {
425                id,
426                commands,
427                thread,
428            } = lane;
429            let (completed, completion) = oneshot::channel();
430            if commands
431                .send(LaneCommand::Shutdown { timeout, completed })
432                .await
433                .is_ok()
434            {
435                completions.push((id.clone(), completion));
436            } else if first_error.is_none() {
437                first_error = Some(ReplicatedRunnerError::LaneUnavailable {
438                    lane: id.to_string(),
439                });
440            }
441            drop(commands);
442            threads.push((id, thread));
443        }
444
445        for (lane, completion) in completions {
446            match completion.await {
447                Ok(ShutdownOutcome::Clean) => {}
448                Ok(outcome) if first_error.is_none() => {
449                    first_error = Some(ReplicatedRunnerError::LaneShutdown {
450                        lane: lane.to_string(),
451                        outcome,
452                    });
453                }
454                Err(_) if first_error.is_none() => {
455                    first_error = Some(ReplicatedRunnerError::LaneUnavailable {
456                        lane: lane.to_string(),
457                    });
458                }
459                _ => {}
460            }
461        }
462        for (lane, thread) in threads {
463            if thread.join().is_err() && first_error.is_none() {
464                first_error = Some(ReplicatedRunnerError::LanePanicked {
465                    lane: lane.to_string(),
466                });
467            }
468        }
469        match first_error {
470            Some(error) => Err(error),
471            None => Ok(()),
472        }
473    }
474}
475
476fn stop_lanes(lanes: BTreeMap<ExecutionLaneId, LaneHandle>) {
477    let mut threads = Vec::new();
478    for (_, lane) in lanes {
479        let (completed, _) = oneshot::channel();
480        let _ = lane.commands.try_send(LaneCommand::Shutdown {
481            timeout: Duration::from_secs(1),
482            completed,
483        });
484        threads.push(lane.thread);
485    }
486    for thread in threads {
487        let _ = thread.join();
488    }
489}
490
491fn singular_binding<'a, C: RequestCapability>(
492    plan: &'a ResolvedAppPlan,
493    caller_instance: &str,
494) -> Result<&'a CapabilityBinding, RuntimeFailure> {
495    let bindings = plan
496        .capability_bindings()
497        .iter()
498        .filter(|binding| {
499            binding.consumer_instance() == caller_instance && binding.capability_id() == C::ID
500        })
501        .collect::<Vec<_>>();
502    match bindings.as_slice() {
503        [binding] => Ok(*binding),
504        [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
505        bindings => Err(RuntimeFailure::AmbiguousBinding {
506            capability: C::ID,
507            providers: bindings.len(),
508        }),
509    }
510}
511
512fn run_lane<F>(
513    lane: ExecutionLaneId,
514    plan: ResolvedAppPlan,
515    mut commands: mpsc::Receiver<LaneCommand>,
516    started: std_mpsc::SyncSender<Result<(), String>>,
517    adapters: Arc<F>,
518    proxy_adapter: LaneProxyAdapter,
519    diagnostics: Arc<LaneDiagnosticsState>,
520    epoch: Instant,
521) where
522    F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
523{
524    let runtime = match tokio::runtime::Builder::new_current_thread()
525        .enable_all()
526        .build()
527    {
528        Ok(runtime) => runtime,
529        Err(error) => {
530            let _ = started.send(Err(error.to_string()));
531            return;
532        }
533    };
534    let local = tokio::task::LocalSet::new();
535    local.block_on(&runtime, async move {
536        let cpu_started = ThreadTime::now();
537        let catalog = match adapters(&lane).with_adapter(proxy_adapter) {
538            Ok(catalog) => catalog,
539            Err(error) => {
540                let _ = started.send(Err(error.to_string()));
541                return;
542            }
543        };
544        let driver = TokioDriver::with_epoch(epoch);
545        let runtime_diagnostics = RuntimeDiagnostics::new();
546        let observer = runtime_diagnostics
547            .subscribe(DiagnosticFilter::only(DiagnosticSource::Invocation), 2048)
548            .expect("diagnostics capacity is positive");
549        let app = match lenso_kernel::Kernel::start_with_diagnostics(
550            plan,
551            driver,
552            catalog,
553            runtime_diagnostics,
554        )
555        .await
556        {
557            Ok(app) => app,
558            Err(error) => {
559                let _ = started.send(Err(format!("{error:?}")));
560                return;
561            }
562        };
563        let _ = started.send(Ok(()));
564        diagnostics.publish_lane(&lane, &app, cpu_started.elapsed());
565        let mut sample_interval = tokio::time::interval(Duration::from_millis(10));
566
567        loop {
568            tokio::select! {
569                command = commands.recv() => match command {
570                    Some(LaneCommand::Run(task)) => {
571                        tokio::task::spawn_local(task(app.clone()));
572                    }
573                    Some(LaneCommand::Shutdown { timeout, completed }) => {
574                        let outcome = app.shutdown(timeout).await;
575                        let _ = completed.send(outcome);
576                        break;
577                    }
578                    None => {
579                        let _ = app.shutdown(Duration::from_secs(1)).await;
580                        break;
581                    }
582                },
583                _ = sample_interval.tick() => {
584                    diagnostics.publish_lane(&lane, &app, cpu_started.elapsed());
585                }
586            }
587            while let Some(record) = observer.try_recv() {
588                if let DiagnosticEvent::InvocationStarted {
589                    caller_instance: Some(caller),
590                    provider_instance: Some(provider),
591                    ..
592                } = record.event
593                {
594                    diagnostics.record_invocation(&lane, &caller, &provider);
595                }
596            }
597        }
598    });
599}