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, future::LocalBoxFuture};
14use lenso_app_plan::{CapabilityBinding, ExecutionLaneId, ResolvedAppPlan};
15use lenso_kernel::{
16    CancellationToken, DiagnosticEvent, DiagnosticFilter, DiagnosticSource,
17    ExecutionAdapterCatalog, NativeApp, NativeRequestHandle, RequestCapability, RuntimeDiagnostics,
18    RuntimeFailure, ShutdownOutcome,
19};
20use tokio::sync::{mpsc, watch};
21
22use crate::TokioDriver;
23
24mod diagnostics;
25mod projection;
26mod transfer;
27
28pub use diagnostics::LaneDiagnosticsSnapshot;
29use diagnostics::LaneDiagnosticsState;
30use projection::{LaneProxyAdapter, project_lane};
31pub use transfer::CrossLaneRequestCatalog;
32
33const LANE_PROXY_EXECUTION_CLASS: &str = "lenso.native-lane-proxy@1";
34
35/// Placement-independent controls applied by the provider lane's Runtime Driver.
36#[derive(Clone, Debug, Default)]
37pub struct LaneInvocationOptions {
38    timeout: Option<Duration>,
39    cancellation: Option<LaneCancellationToken>,
40}
41
42impl LaneInvocationOptions {
43    /// Creates an invocation without a deadline.
44    pub const fn new() -> Self {
45        Self {
46            timeout: None,
47            cancellation: None,
48        }
49    }
50
51    /// Applies a provider-Driver-relative deadline.
52    #[must_use]
53    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
54        self.timeout = Some(timeout);
55        self
56    }
57
58    /// Propagates one caller-owned cooperative cancellation signal.
59    #[must_use]
60    pub fn with_cancellation(mut self, cancellation: LaneCancellationToken) -> Self {
61        self.cancellation = Some(cancellation);
62        self
63    }
64}
65
66/// A thread-safe caller signal translated to a lane-local Kernel cancellation token.
67#[derive(Clone, Debug)]
68pub struct LaneCancellationToken {
69    cancelled: watch::Sender<bool>,
70}
71
72impl Default for LaneCancellationToken {
73    fn default() -> Self {
74        let (cancelled, _) = watch::channel(false);
75        Self { cancelled }
76    }
77}
78
79impl LaneCancellationToken {
80    /// Creates a signal that has not been cancelled.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Requests cooperative cancellation of attached invocations.
86    pub fn cancel(&self) {
87        self.cancelled.send_replace(true);
88    }
89
90    /// Returns whether cancellation was requested.
91    pub fn is_cancelled(&self) -> bool {
92        *self.cancelled.borrow()
93    }
94
95    async fn cancelled(&self) {
96        let mut cancelled = self.cancelled.subscribe();
97        loop {
98            if *cancelled.borrow_and_update() {
99                return;
100            }
101            if cancelled.changed().await.is_err() {
102                return;
103            }
104        }
105    }
106}
107
108type LaneTask = Box<dyn FnOnce(LaneRuntime) -> LocalBoxFuture<'static, ()> + Send + 'static>;
109type LaneSender = mpsc::Sender<LaneTask>;
110type LaneRoute = mpsc::WeakSender<LaneTask>;
111
112struct LaneShutdown {
113    timeout: Duration,
114    completed: oneshot::Sender<ShutdownOutcome>,
115}
116
117struct LaneHandle {
118    id: ExecutionLaneId,
119    commands: LaneSender,
120    shutdown: oneshot::Sender<LaneShutdown>,
121    thread: thread::JoinHandle<()>,
122}
123
124type TypedRequestHandles = HashMap<String, Box<dyn Any>>;
125
126#[derive(Clone)]
127struct LaneRuntime {
128    app: NativeApp,
129    request_handles: Rc<RefCell<HashMap<TypeId, TypedRequestHandles>>>,
130}
131
132impl LaneRuntime {
133    fn new(app: NativeApp) -> Self {
134        Self {
135            app,
136            request_handles: Rc::new(RefCell::new(HashMap::new())),
137        }
138    }
139
140    fn request_handle<C: RequestCapability>(
141        &self,
142        caller_instance: &str,
143    ) -> Result<Rc<NativeRequestHandle<C>>, RuntimeFailure> {
144        let capability = TypeId::of::<C>();
145        if let Some(handle) = self
146            .request_handles
147            .borrow()
148            .get(&capability)
149            .and_then(|handles| handles.get(caller_instance))
150            .and_then(|handle| handle.downcast_ref::<Rc<NativeRequestHandle<C>>>())
151        {
152            return Ok(handle.clone());
153        }
154        let handle = Rc::new(self.app.handle::<C>(caller_instance)?);
155        self.request_handles
156            .borrow_mut()
157            .entry(capability)
158            .or_default()
159            .insert(caller_instance.to_owned(), Box::new(handle.clone()));
160        Ok(handle)
161    }
162}
163
164/// A native Runner startup or lane-lifecycle failure.
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub enum ReplicatedRunnerError {
167    /// The immutable Plan failed validation before any lane started.
168    InvalidPlan { detail: String },
169    /// One lane could not construct or start its Kernel replica.
170    LaneStartup { lane: String, detail: String },
171    /// A generated request Capability was not registered with the native transfer catalog.
172    MissingCrossLaneRequestTransfer { capability: String },
173    /// A lane stopped accepting Runner commands unexpectedly.
174    LaneUnavailable { lane: String },
175    /// A lane thread panicked while stopping.
176    LanePanicked { lane: String },
177    /// One Kernel replica did not stop cleanly.
178    LaneShutdown {
179        lane: String,
180        outcome: ShutdownOutcome,
181    },
182}
183
184impl fmt::Display for ReplicatedRunnerError {
185    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186        match self {
187            Self::InvalidPlan { detail } => {
188                write!(formatter, "invalid Resolved App Plan: {detail}")
189            }
190            Self::LaneStartup { lane, detail } => {
191                write!(
192                    formatter,
193                    "Execution Lane `{lane}` failed to start: {detail}"
194                )
195            }
196            Self::MissingCrossLaneRequestTransfer { capability } => write!(
197                formatter,
198                "Capability `{capability}` has no registered native cross-lane request transfer"
199            ),
200            Self::LaneUnavailable { lane } => {
201                write!(formatter, "Execution Lane `{lane}` is unavailable")
202            }
203            Self::LanePanicked { lane } => write!(formatter, "Execution Lane `{lane}` panicked"),
204            Self::LaneShutdown { lane, outcome } => write!(
205                formatter,
206                "Execution Lane `{lane}` stopped with {outcome:?}"
207            ),
208        }
209    }
210}
211
212impl std::error::Error for ReplicatedRunnerError {}
213
214/// One native App executed as a fixed set of Plan-declared single-owner Kernel lanes.
215pub struct ReplicatedNativeApp {
216    plan: Arc<ResolvedAppPlan>,
217    lanes: BTreeMap<ExecutionLaneId, LaneHandle>,
218    diagnostics: Arc<LaneDiagnosticsState>,
219    epoch: Instant,
220}
221
222impl fmt::Debug for ReplicatedNativeApp {
223    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224        formatter
225            .debug_struct("ReplicatedNativeApp")
226            .field("lanes", &self.lanes.keys().collect::<Vec<_>>())
227            .finish_non_exhaustive()
228    }
229}
230
231impl ReplicatedNativeApp {
232    /// Starts one unmodified Kernel replica per declared Execution Lane.
233    pub fn start<F>(plan: ResolvedAppPlan, adapters: F) -> Result<Self, ReplicatedRunnerError>
234    where
235        F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
236    {
237        Self::start_with_transfers(plan, adapters, CrossLaneRequestCatalog::new())
238    }
239
240    /// Starts replicated lanes with generated request types allowed to cross them.
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 _ = 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 lane =
424            self.lanes
425                .get(consumer.execution_lane())
426                .ok_or_else(|| RuntimeFailure::Internal {
427                    detail: format!(
428                        "Execution Lane `{}` is unavailable",
429                        consumer.execution_lane()
430                    ),
431                })?;
432        let caller_instance = caller_instance.to_owned();
433        let operation = operation.to_owned();
434        let deadline = options
435            .timeout
436            .map(|timeout| self.epoch.elapsed().saturating_add(timeout));
437        let (completed, completion) = oneshot::channel();
438        lane.commands
439            .send(Box::new(move |lane| {
440                Box::pin(async move {
441                    let handle = match lane.request_handle::<C>(&caller_instance) {
442                        Ok(handle) => handle,
443                        Err(error) => {
444                            let _ = completed.send(Err(error));
445                            return;
446                        }
447                    };
448                    let cancellation = CancellationToken::new();
449                    let external_cancellation = options.cancellation;
450                    if external_cancellation
451                        .as_ref()
452                        .is_some_and(LaneCancellationToken::is_cancelled)
453                    {
454                        cancellation.cancel();
455                    }
456                    let invocation = if deadline.is_some() || external_cancellation.is_some() {
457                        let context = lane.app.invocation_context(deadline, cancellation.clone());
458                        Either::Left(handle.invoke_with_context(&operation, context, request))
459                    } else {
460                        Either::Right(handle.invoke(&operation, request))
461                    };
462                    tokio::pin!(invocation);
463                    let result = if let Some(external_cancellation) = external_cancellation {
464                        tokio::select! {
465                            result = &mut invocation => result,
466                            () = external_cancellation.cancelled() => {
467                                cancellation.cancel();
468                                invocation.await
469                            }
470                        }
471                    } else {
472                        invocation.await
473                    };
474                    let _ = completed.send(result);
475                })
476            }))
477            .await
478            .map_err(|_| RuntimeFailure::Internal {
479                detail: format!("Execution Lane `{}` is unavailable", lane.id),
480            })?;
481        completion.await.map_err(|_| RuntimeFailure::Internal {
482            detail: format!("Execution Lane `{}` dropped an invocation", lane.id),
483        })?
484    }
485
486    /// Stops every Kernel replica with the same bounded shutdown timeout.
487    pub async fn shutdown(self, timeout: Duration) -> Result<(), ReplicatedRunnerError> {
488        let mut completions = Vec::new();
489        let mut threads = Vec::new();
490        let mut first_error = None;
491        for (_, lane) in self.lanes {
492            let LaneHandle {
493                id,
494                commands,
495                shutdown,
496                thread,
497            } = lane;
498            let (completed, completion) = oneshot::channel();
499            if shutdown.send(LaneShutdown { timeout, completed }).is_ok() {
500                completions.push((id.clone(), completion));
501            } else if first_error.is_none() {
502                first_error = Some(ReplicatedRunnerError::LaneUnavailable {
503                    lane: id.to_string(),
504                });
505            }
506            drop(commands);
507            threads.push((id, thread));
508        }
509
510        for (lane, completion) in completions {
511            match completion.await {
512                Ok(ShutdownOutcome::Clean) => {}
513                Ok(outcome) if first_error.is_none() => {
514                    first_error = Some(ReplicatedRunnerError::LaneShutdown {
515                        lane: lane.to_string(),
516                        outcome,
517                    });
518                }
519                Err(_) if first_error.is_none() => {
520                    first_error = Some(ReplicatedRunnerError::LaneUnavailable {
521                        lane: lane.to_string(),
522                    });
523                }
524                _ => {}
525            }
526        }
527        for (lane, thread) in threads {
528            if thread.join().is_err() && first_error.is_none() {
529                first_error = Some(ReplicatedRunnerError::LanePanicked {
530                    lane: lane.to_string(),
531                });
532            }
533        }
534        match first_error {
535            Some(error) => Err(error),
536            None => Ok(()),
537        }
538    }
539}
540
541fn stop_lanes(lanes: BTreeMap<ExecutionLaneId, LaneHandle>) {
542    let mut threads = Vec::new();
543    for (_, lane) in lanes {
544        let (completed, _) = oneshot::channel();
545        let _ = lane.shutdown.send(LaneShutdown {
546            timeout: Duration::from_secs(1),
547            completed,
548        });
549        threads.push(lane.thread);
550    }
551    for thread in threads {
552        let _ = thread.join();
553    }
554}
555
556fn singular_binding<'a, C: RequestCapability>(
557    plan: &'a ResolvedAppPlan,
558    caller_instance: &str,
559) -> Result<&'a CapabilityBinding, RuntimeFailure> {
560    let mut bindings = plan.capability_bindings().iter().filter(|binding| {
561        binding.consumer_instance() == caller_instance && binding.capability_id() == C::ID
562    });
563    let Some(binding) = bindings.next() else {
564        return Err(RuntimeFailure::Unavailable { capability: C::ID });
565    };
566    let providers = 1 + bindings.count();
567    if providers == 1 {
568        Ok(binding)
569    } else {
570        Err(RuntimeFailure::AmbiguousBinding {
571            capability: C::ID,
572            providers,
573        })
574    }
575}
576
577fn run_lane<F>(
578    lane: ExecutionLaneId,
579    plan: ResolvedAppPlan,
580    mut commands: mpsc::Receiver<LaneTask>,
581    mut shutdown: oneshot::Receiver<LaneShutdown>,
582    started: std_mpsc::SyncSender<Result<(), String>>,
583    adapters: Arc<F>,
584    proxy_adapter: LaneProxyAdapter,
585    diagnostics: Arc<LaneDiagnosticsState>,
586    epoch: Instant,
587) where
588    F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
589{
590    let runtime = match tokio::runtime::Builder::new_current_thread()
591        .enable_all()
592        .build()
593    {
594        Ok(runtime) => runtime,
595        Err(error) => {
596            let _ = started.send(Err(error.to_string()));
597            return;
598        }
599    };
600    let local = tokio::task::LocalSet::new();
601    local.block_on(&runtime, async move {
602        let cpu_started = ThreadTime::now();
603        let catalog = match adapters(&lane).with_adapter(proxy_adapter) {
604            Ok(catalog) => catalog,
605            Err(error) => {
606                let _ = started.send(Err(error.to_string()));
607                return;
608            }
609        };
610        let driver = TokioDriver::with_epoch(epoch);
611        let runtime_diagnostics = RuntimeDiagnostics::new();
612        let observer = runtime_diagnostics
613            .subscribe(DiagnosticFilter::only(DiagnosticSource::Invocation), 2048)
614            .expect("diagnostics capacity is positive");
615        let app = match lenso_kernel::Kernel::start_with_diagnostics(
616            plan,
617            driver,
618            catalog,
619            runtime_diagnostics,
620        )
621        .await
622        {
623            Ok(app) => app,
624            Err(error) => {
625                let _ = started.send(Err(format!("{error:?}")));
626                return;
627            }
628        };
629        let lane_runtime = LaneRuntime::new(app.clone());
630        let _ = started.send(Ok(()));
631        diagnostics.publish_lane(&lane, &app, cpu_started.elapsed());
632        let mut sample_interval = tokio::time::interval(Duration::from_millis(10));
633
634        loop {
635            tokio::select! {
636                biased;
637                shutdown = &mut shutdown => {
638                    match shutdown {
639                        Ok(LaneShutdown { timeout, completed }) => {
640                            let outcome = app.shutdown(timeout).await;
641                            let _ = completed.send(outcome);
642                        }
643                        Err(_) => {
644                            let _ = app.shutdown(Duration::from_secs(1)).await;
645                        }
646                    }
647                    break;
648                }
649                command = commands.recv() => if let Some(task) = command {
650                    tokio::task::spawn_local(task(lane_runtime.clone()));
651                } else {
652                    let _ = app.shutdown(Duration::from_secs(1)).await;
653                    break;
654                },
655                _ = sample_interval.tick() => {
656                    diagnostics.publish_lane(&lane, &app, cpu_started.elapsed());
657                }
658            }
659            while let Some(record) = observer.try_recv() {
660                if let DiagnosticEvent::InvocationStarted {
661                    caller_instance: Some(caller),
662                    provider_instance: Some(provider),
663                    ..
664                } = record.event
665                {
666                    diagnostics.record_invocation(&lane, &caller, &provider);
667                }
668            }
669        }
670    });
671}