Skip to main content

lenso_runner/
replicated.rs

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