1use std::{
2 any::{Any, TypeId},
3 cell::RefCell,
4 collections::{BTreeMap, HashMap},
5 fmt,
6 panic::{AssertUnwindSafe, catch_unwind},
7 rc::Rc,
8 sync::{Arc, mpsc as std_mpsc},
9 thread,
10 time::{Duration, Instant},
11};
12
13use cpu_time::ThreadTime;
14use futures::{channel::oneshot, future::Either};
15use lenso_app_plan::{CapabilityBinding, ExecutionLaneId, ResolvedAppPlan};
16use lenso_kernel::{
17 CancellationToken, EventCapability, ExecutionAdapterCatalog, NativeApp, NativeEventHandle,
18 NativeRequestHandle, NativeStream, NativeStreamHandle, RequestCapability, RuntimeDiagnostics,
19 RuntimeFailure, ShutdownOutcome, StreamCapability,
20};
21use tokio::sync::{mpsc, watch};
22
23use crate::TokioDriver;
24
25mod diagnostics;
26mod error;
27mod interaction_transfer;
28mod projection;
29mod terminal;
30mod transfer;
31
32pub use diagnostics::LaneDiagnosticsSnapshot;
33use diagnostics::{LaneDiagnosticsState, LaneInvocationProbe};
34pub use error::ReplicatedRunnerError;
35use interaction_transfer::CrossLaneInteractionCatalog;
36use projection::{LaneProxyAdapter, project_lane};
37use terminal::ReplicatedTerminalState;
38pub use transfer::CrossLaneRequestCatalog;
39
40const LANE_PROXY_EXECUTION_CLASS: &str = "lenso.native-lane-proxy@1";
41
42#[derive(Clone, Debug, Default)]
44pub struct LaneInvocationOptions {
45 timeout: Option<Duration>,
46 cancellation: Option<LaneCancellationToken>,
47}
48
49impl LaneInvocationOptions {
50 pub const fn new() -> Self {
52 Self {
53 timeout: None,
54 cancellation: None,
55 }
56 }
57
58 #[must_use]
60 pub const fn with_timeout(mut self, timeout: Duration) -> Self {
61 self.timeout = Some(timeout);
62 self
63 }
64
65 #[must_use]
67 pub fn with_cancellation(mut self, cancellation: LaneCancellationToken) -> Self {
68 self.cancellation = Some(cancellation);
69 self
70 }
71}
72
73#[derive(Clone, Debug)]
75pub struct LaneCancellationToken {
76 cancelled: watch::Sender<bool>,
77}
78
79impl Default for LaneCancellationToken {
80 fn default() -> Self {
81 let (cancelled, _) = watch::channel(false);
82 Self { cancelled }
83 }
84}
85
86impl LaneCancellationToken {
87 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn cancel(&self) {
94 self.cancelled.send_replace(true);
95 }
96
97 pub fn is_cancelled(&self) -> bool {
99 *self.cancelled.borrow()
100 }
101
102 async fn cancelled(&self) {
103 let mut cancelled = self.cancelled.subscribe();
104 loop {
105 if *cancelled.borrow_and_update() {
106 return;
107 }
108 if cancelled.changed().await.is_err() {
109 return;
110 }
111 }
112 }
113}
114
115type LaneTask = Box<dyn FnOnce(LaneRuntime) + Send + 'static>;
116type LaneSender = mpsc::Sender<LaneTask>;
117type LaneRoute = mpsc::WeakSender<LaneTask>;
118
119struct LaneShutdown {
120 timeout: Duration,
121 completed: oneshot::Sender<ShutdownOutcome>,
122}
123
124struct LaneHandle {
125 id: ExecutionLaneId,
126 commands: LaneSender,
127 shutdown: oneshot::Sender<LaneShutdown>,
128 thread: thread::JoinHandle<()>,
129}
130
131type TypedRequestHandles = HashMap<String, Box<dyn Any>>;
132type TypedStreamSessions = HashMap<(TypeId, u64), Box<dyn Any>>;
133
134#[derive(Clone)]
135struct LaneRuntime {
136 app: NativeApp,
137 request_handles: Rc<RefCell<HashMap<TypeId, TypedRequestHandles>>>,
138 stream_sessions: Rc<RefCell<TypedStreamSessions>>,
139}
140
141impl LaneRuntime {
142 fn new(app: NativeApp) -> Self {
143 Self {
144 app,
145 request_handles: Rc::new(RefCell::new(HashMap::new())),
146 stream_sessions: Rc::new(RefCell::new(HashMap::new())),
147 }
148 }
149
150 fn request_handle<C: RequestCapability>(
151 &self,
152 caller_instance: &str,
153 ) -> Result<Rc<NativeRequestHandle<C>>, RuntimeFailure> {
154 let capability = TypeId::of::<C>();
155 if let Some(handle) = self
156 .request_handles
157 .borrow()
158 .get(&capability)
159 .and_then(|handles| handles.get(caller_instance))
160 .and_then(|handle| handle.downcast_ref::<Rc<NativeRequestHandle<C>>>())
161 {
162 return Ok(handle.clone());
163 }
164 let handle = Rc::new(self.app.handle::<C>(caller_instance)?);
165 self.request_handles
166 .borrow_mut()
167 .entry(capability)
168 .or_default()
169 .insert(caller_instance.to_owned(), Box::new(handle.clone()));
170 Ok(handle)
171 }
172
173 fn stream_handle<C: StreamCapability>(
174 &self,
175 caller_instance: &str,
176 provider_instance: &str,
177 ) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
178 let dependencies = self.app.dependencies(caller_instance)?;
179 dependencies
180 .bindings()
181 .iter()
182 .find(|binding| {
183 binding.capability_id() == C::ID && binding.provider_instance() == provider_instance
184 })
185 .and_then(lenso_kernel::ModuleDependency::stream_handle)
186 .ok_or(RuntimeFailure::Unavailable { capability: C::ID })?
187 .typed::<C>()
188 }
189
190 fn event_handle<C: EventCapability>(
191 &self,
192 caller_instance: &str,
193 provider_instance: &str,
194 ) -> Result<NativeEventHandle<C>, RuntimeFailure> {
195 let dependencies = self.app.dependencies(caller_instance)?;
196 dependencies
197 .bindings()
198 .iter()
199 .find(|binding| {
200 binding.capability_id() == C::ID && binding.provider_instance() == provider_instance
201 })
202 .and_then(lenso_kernel::ModuleDependency::event_handle)
203 .ok_or(RuntimeFailure::Unavailable { capability: C::ID })?
204 .typed::<C>()
205 }
206
207 fn insert_stream<C: StreamCapability>(&self, session_id: u64, stream: NativeStream<C>) {
208 self.stream_sessions
209 .borrow_mut()
210 .insert((TypeId::of::<C>(), session_id), Box::new(Rc::new(stream)));
211 }
212
213 fn stream<C: StreamCapability>(
214 &self,
215 session_id: u64,
216 ) -> Result<Rc<NativeStream<C>>, RuntimeFailure> {
217 self.stream_sessions
218 .borrow()
219 .get(&(TypeId::of::<C>(), session_id))
220 .and_then(|stream| stream.downcast_ref::<Rc<NativeStream<C>>>())
221 .cloned()
222 .ok_or(RuntimeFailure::Unavailable { capability: C::ID })
223 }
224
225 fn remove_stream<C: StreamCapability>(&self, session_id: u64) {
226 self.stream_sessions
227 .borrow_mut()
228 .remove(&(TypeId::of::<C>(), session_id));
229 }
230}
231
232#[derive(Clone, Debug, Default)]
234pub struct CrossLaneTransferCatalog {
235 requests: CrossLaneRequestCatalog,
236 interactions: CrossLaneInteractionCatalog,
237}
238
239impl CrossLaneTransferCatalog {
240 pub fn new() -> Self {
242 Self::default()
243 }
244
245 #[must_use]
247 pub fn with_request<C>(mut self, operations: &'static [&'static str]) -> Self
248 where
249 C: RequestCapability,
250 C::Request: Send,
251 C::Response: Send,
252 C::DomainError: Send,
253 {
254 self.requests = self.requests.with_request::<C>(operations);
255 self
256 }
257
258 #[must_use]
260 pub fn with_stream<C>(mut self, operations: &'static [&'static str]) -> Self
261 where
262 C: StreamCapability,
263 C::OpenRequest: Send,
264 C::Message: Send,
265 C::DomainError: Send,
266 {
267 self.interactions = self.interactions.with_stream::<C>(operations);
268 self
269 }
270
271 #[must_use]
273 pub fn with_event<C>(mut self, operations: &'static [&'static str]) -> Self
274 where
275 C: EventCapability,
276 C::Event: Send,
277 {
278 self.interactions = self.interactions.with_event::<C>(operations);
279 self
280 }
281
282 fn validate_plan(&self, plan: &ResolvedAppPlan) -> Result<(), ReplicatedRunnerError> {
283 self.requests.validate_plan(plan)?;
284 self.interactions.validate_plan(plan)
285 }
286}
287
288impl From<CrossLaneRequestCatalog> for CrossLaneTransferCatalog {
289 fn from(requests: CrossLaneRequestCatalog) -> Self {
290 Self {
291 requests,
292 interactions: CrossLaneInteractionCatalog::default(),
293 }
294 }
295}
296
297pub struct ReplicatedNativeApp {
299 plan: Arc<ResolvedAppPlan>,
300 lanes: BTreeMap<ExecutionLaneId, LaneHandle>,
301 diagnostics: Arc<LaneDiagnosticsState>,
302 terminal: Arc<ReplicatedTerminalState>,
303 epoch: Instant,
304}
305
306impl fmt::Debug for ReplicatedNativeApp {
307 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308 formatter
309 .debug_struct("ReplicatedNativeApp")
310 .field("lanes", &self.lanes.keys().collect::<Vec<_>>())
311 .finish_non_exhaustive()
312 }
313}
314
315impl ReplicatedNativeApp {
316 fn ensure_running(&self) -> Result<(), RuntimeFailure> {
317 if let Some(failure) = self.terminal.failure() {
318 return Err(RuntimeFailure::Internal {
319 detail: failure.to_string(),
320 });
321 }
322 Ok(())
323 }
324
325 pub fn start<F>(plan: ResolvedAppPlan, adapters: F) -> Result<Self, ReplicatedRunnerError>
327 where
328 F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
329 {
330 Self::start_with_transfer_catalog(plan, adapters, CrossLaneTransferCatalog::new())
331 }
332
333 #[allow(clippy::needless_pass_by_value, clippy::too_many_lines)]
335 pub fn start_with_transfers<F>(
336 plan: ResolvedAppPlan,
337 adapters: F,
338 transfers: CrossLaneRequestCatalog,
339 ) -> Result<Self, ReplicatedRunnerError>
340 where
341 F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
342 {
343 Self::start_with_transfer_catalog(plan, adapters, transfers.into())
344 }
345
346 #[allow(clippy::needless_pass_by_value, clippy::too_many_lines)]
348 pub fn start_with_transfer_catalog<F>(
349 plan: ResolvedAppPlan,
350 adapters: F,
351 transfers: CrossLaneTransferCatalog,
352 ) -> Result<Self, ReplicatedRunnerError>
353 where
354 F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
355 {
356 plan.validate()
357 .map_err(|error| ReplicatedRunnerError::InvalidPlan {
358 detail: error.to_string(),
359 })?;
360 transfers.validate_plan(&plan)?;
361 let plan = Arc::new(plan);
362 let adapters = Arc::new(adapters);
363 let diagnostics = Arc::new(LaneDiagnosticsState::new(Arc::clone(&plan)));
364 let terminal = Arc::new(ReplicatedTerminalState::default());
365 let epoch = Instant::now();
366 let mut receivers = BTreeMap::new();
367 let senders = plan
368 .execution_lanes()
369 .iter()
370 .map(|lane| {
371 let (sender, receiver) = mpsc::channel(64);
372 receivers.insert(lane.id().clone(), receiver);
373 (lane.id().clone(), sender)
374 })
375 .collect::<BTreeMap<_, _>>();
376 let routes = Arc::new(
377 senders
378 .iter()
379 .map(|(lane, sender)| (lane.clone(), sender.downgrade()))
380 .collect::<BTreeMap<_, _>>(),
381 );
382 let projected = plan
383 .execution_lanes()
384 .iter()
385 .map(|lane| {
386 project_lane(&plan, lane.id()).map(|projected| (lane.id().clone(), projected))
387 })
388 .collect::<Result<Vec<_>, _>>()?;
389 let mut lanes = BTreeMap::new();
390 let mut startups = Vec::new();
391
392 for (lane_id, lane_plan) in projected {
393 let commands = senders
394 .get(&lane_id)
395 .expect("every declared lane has a command route")
396 .clone();
397 let receiver = receivers
398 .remove(&lane_id)
399 .expect("every declared lane has one command receiver");
400 let (shutdown, shutdown_request) = oneshot::channel();
401 let (started, startup) = std_mpsc::sync_channel(1);
402 let lane_adapters = Arc::clone(&adapters);
403 let lane_diagnostics = Arc::clone(&diagnostics);
404 let lane_terminal = Arc::clone(&terminal);
405 let proxy_adapter = LaneProxyAdapter::new(
406 Arc::clone(&plan),
407 transfers.clone(),
408 Arc::clone(&routes),
409 epoch,
410 );
411 let thread_lane = lane_id.clone();
412 let lane_thread = match thread::Builder::new()
413 .name(format!("lenso-lane-{}", lane_id.as_str()))
414 .spawn(move || {
415 let reported_lane = thread_lane.clone();
416 let result = catch_unwind(AssertUnwindSafe(|| {
417 run_lane(
418 thread_lane,
419 lane_plan,
420 receiver,
421 shutdown_request,
422 started,
423 lane_adapters,
424 proxy_adapter,
425 lane_diagnostics,
426 Arc::clone(&lane_terminal),
427 epoch,
428 );
429 }));
430 if result.is_err() {
431 lane_terminal.fail(ReplicatedRunnerError::LanePanicked {
432 lane: reported_lane.to_string(),
433 });
434 }
435 }) {
436 Ok(thread) => thread,
437 Err(error) => {
438 drop(receivers);
439 drop(routes);
440 drop(senders);
441 terminal.begin_shutdown();
442 stop_lanes(lanes);
443 return Err(ReplicatedRunnerError::LaneStartup {
444 lane: lane_id.to_string(),
445 detail: error.to_string(),
446 });
447 }
448 };
449 startups.push((lane_id.clone(), startup));
450 lanes.insert(
451 lane_id.clone(),
452 LaneHandle {
453 id: lane_id,
454 commands,
455 shutdown,
456 thread: lane_thread,
457 },
458 );
459 }
460
461 for (lane, startup) in startups {
462 match startup.recv() {
463 Ok(Ok(())) => {}
464 Ok(Err(detail)) => {
465 drop(routes);
466 drop(senders);
467 terminal.begin_shutdown();
468 stop_lanes(lanes);
469 return Err(ReplicatedRunnerError::LaneStartup {
470 lane: lane.to_string(),
471 detail,
472 });
473 }
474 Err(_) => {
475 drop(routes);
476 drop(senders);
477 let failure = terminal.failure().unwrap_or_else(|| {
478 ReplicatedRunnerError::LaneUnavailable {
479 lane: lane.to_string(),
480 }
481 });
482 terminal.begin_shutdown();
483 stop_lanes(lanes);
484 return Err(failure);
485 }
486 }
487 }
488
489 Ok(Self {
490 plan,
491 lanes,
492 diagnostics,
493 terminal,
494 epoch,
495 })
496 }
497
498 pub fn lane_count(&self) -> usize {
500 self.lanes.len()
501 }
502
503 pub fn diagnostics_snapshot(&self) -> LaneDiagnosticsSnapshot {
505 self.diagnostics.snapshot()
506 }
507
508 pub fn is_failed(&self) -> bool {
510 self.terminal.is_failed()
511 }
512
513 pub fn terminal_failure(&self) -> Option<ReplicatedRunnerError> {
515 self.terminal.failure()
516 }
517
518 pub async fn wait_for_terminal(&self) -> ReplicatedRunnerError {
520 self.terminal.wait().await
521 }
522
523 pub async fn invoke<C: RequestCapability>(
525 &self,
526 caller_instance: &str,
527 operation: &str,
528 request: C::Request,
529 ) -> Result<Result<C::Response, C::DomainError>, RuntimeFailure>
530 where
531 C::Request: Send,
532 C::Response: Send,
533 C::DomainError: Send,
534 {
535 self.invoke_with_options::<C>(
536 caller_instance,
537 operation,
538 request,
539 LaneInvocationOptions::new(),
540 )
541 .await
542 }
543
544 pub async fn invoke_with_options<C: RequestCapability>(
546 &self,
547 caller_instance: &str,
548 operation: &str,
549 request: C::Request,
550 options: LaneInvocationOptions,
551 ) -> Result<Result<C::Response, C::DomainError>, RuntimeFailure>
552 where
553 C::Request: Send,
554 C::Response: Send,
555 C::DomainError: Send,
556 {
557 self.ensure_running()?;
558 let binding = singular_binding::<C>(&self.plan, caller_instance)?;
559 let consumer = self.plan.module_instance(caller_instance).ok_or_else(|| {
560 RuntimeFailure::InvalidResolvedPlan {
561 detail: format!("binding consumer `{caller_instance}` is absent from the Plan"),
562 }
563 })?;
564 let provider = self
565 .plan
566 .module_instance(binding.provider_instance())
567 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
568 detail: format!(
569 "binding provider `{}` is absent from the Plan",
570 binding.provider_instance()
571 ),
572 })?;
573 let lane =
576 self.lanes
577 .get(provider.execution_lane())
578 .ok_or_else(|| RuntimeFailure::Internal {
579 detail: format!(
580 "Execution Lane `{}` is unavailable",
581 provider.execution_lane()
582 ),
583 })?;
584 let cross_lane_diagnostics =
585 (consumer.execution_lane() != provider.execution_lane()).then(|| {
586 (
587 Arc::clone(&self.diagnostics),
588 consumer.execution_lane().clone(),
589 binding.provider_instance().to_owned(),
590 )
591 });
592 let caller_instance = caller_instance.to_owned();
593 let operation = operation.to_owned();
594 let deadline = options
595 .timeout
596 .map(|timeout| self.epoch.elapsed().saturating_add(timeout));
597 let (completed, completion) = oneshot::channel();
598 lane.commands
599 .send(Box::new(move |lane| {
600 if let Some((diagnostics, caller_lane, provider_instance)) = cross_lane_diagnostics
601 {
602 diagnostics.record_invocation(
603 &caller_lane,
604 &caller_instance,
605 &provider_instance,
606 );
607 }
608 tokio::task::spawn_local(async move {
609 let handle = match lane.request_handle::<C>(&caller_instance) {
610 Ok(handle) => handle,
611 Err(error) => {
612 let _ = completed.send(Err(error));
613 return;
614 }
615 };
616 let cancellation = CancellationToken::new();
617 let external_cancellation = options.cancellation;
618 if external_cancellation
619 .as_ref()
620 .is_some_and(LaneCancellationToken::is_cancelled)
621 {
622 cancellation.cancel();
623 }
624 let invocation = if deadline.is_some() || external_cancellation.is_some() {
625 let context = lane.app.invocation_context(deadline, cancellation.clone());
626 Either::Left(handle.invoke_with_context(&operation, context, request))
627 } else {
628 Either::Right(handle.invoke(&operation, request))
629 };
630 tokio::pin!(invocation);
631 let result = if let Some(external_cancellation) = external_cancellation {
632 tokio::select! {
633 result = &mut invocation => result,
634 () = external_cancellation.cancelled() => {
635 cancellation.cancel();
636 invocation.await
637 }
638 }
639 } else {
640 invocation.await
641 };
642 let _ = completed.send(result);
643 });
644 }))
645 .await
646 .map_err(|_| RuntimeFailure::Internal {
647 detail: format!("Execution Lane `{}` is unavailable", lane.id),
648 })?;
649 completion.await.map_err(|_| RuntimeFailure::Internal {
650 detail: format!("Execution Lane `{}` dropped an invocation", lane.id),
651 })?
652 }
653
654 pub async fn shutdown(self, timeout: Duration) -> Result<(), ReplicatedRunnerError> {
656 self.terminal.begin_shutdown();
657 let mut completions = Vec::new();
658 let mut threads = Vec::new();
659 let mut first_error = self.terminal.failure();
660 for (_, lane) in self.lanes {
661 let LaneHandle {
662 id,
663 commands,
664 shutdown,
665 thread,
666 } = lane;
667 let (completed, completion) = oneshot::channel();
668 if shutdown.send(LaneShutdown { timeout, completed }).is_ok() {
669 completions.push((id.clone(), completion));
670 } else if first_error.is_none() {
671 first_error = Some(ReplicatedRunnerError::LaneUnavailable {
672 lane: id.to_string(),
673 });
674 }
675 drop(commands);
676 threads.push((id, thread));
677 }
678
679 for (lane, completion) in completions {
680 match completion.await {
681 Ok(ShutdownOutcome::Clean) => {}
682 Ok(outcome) if first_error.is_none() => {
683 first_error = Some(ReplicatedRunnerError::LaneShutdown {
684 lane: lane.to_string(),
685 outcome,
686 });
687 }
688 Err(_) if first_error.is_none() => {
689 first_error = Some(ReplicatedRunnerError::LaneUnavailable {
690 lane: lane.to_string(),
691 });
692 }
693 _ => {}
694 }
695 }
696 for (lane, thread) in threads {
697 if thread.join().is_err() && first_error.is_none() {
698 first_error = Some(ReplicatedRunnerError::LanePanicked {
699 lane: lane.to_string(),
700 });
701 }
702 }
703 match first_error {
704 Some(error) => Err(error),
705 None => Ok(()),
706 }
707 }
708}
709
710fn stop_lanes(lanes: BTreeMap<ExecutionLaneId, LaneHandle>) {
711 let mut threads = Vec::new();
712 for (_, lane) in lanes {
713 let (completed, _) = oneshot::channel();
714 let _ = lane.shutdown.send(LaneShutdown {
715 timeout: Duration::from_secs(1),
716 completed,
717 });
718 threads.push(lane.thread);
719 }
720 for thread in threads {
721 let _ = thread.join();
722 }
723}
724
725fn singular_binding<'a, C: RequestCapability>(
726 plan: &'a ResolvedAppPlan,
727 caller_instance: &str,
728) -> Result<&'a CapabilityBinding, RuntimeFailure> {
729 let mut bindings = plan.capability_bindings().iter().filter(|binding| {
730 binding.consumer_instance() == caller_instance && binding.capability_id() == C::ID
731 });
732 let Some(binding) = bindings.next() else {
733 return Err(RuntimeFailure::Unavailable { capability: C::ID });
734 };
735 let providers = 1 + bindings.count();
736 if providers == 1 {
737 Ok(binding)
738 } else {
739 Err(RuntimeFailure::AmbiguousBinding {
740 capability: C::ID,
741 providers,
742 })
743 }
744}
745
746#[allow(clippy::too_many_arguments)]
747fn run_lane<F>(
748 lane: ExecutionLaneId,
749 plan: ResolvedAppPlan,
750 mut commands: mpsc::Receiver<LaneTask>,
751 mut shutdown: oneshot::Receiver<LaneShutdown>,
752 started: std_mpsc::SyncSender<Result<(), String>>,
753 adapters: Arc<F>,
754 proxy_adapter: LaneProxyAdapter,
755 diagnostics: Arc<LaneDiagnosticsState>,
756 terminal: Arc<ReplicatedTerminalState>,
757 epoch: Instant,
758) where
759 F: Fn(&ExecutionLaneId) -> ExecutionAdapterCatalog + Send + Sync + 'static,
760{
761 let runtime = match tokio::runtime::Builder::new_current_thread()
762 .enable_all()
763 .build()
764 {
765 Ok(runtime) => runtime,
766 Err(error) => {
767 let _ = started.send(Err(error.to_string()));
768 return;
769 }
770 };
771 let local = tokio::task::LocalSet::new();
772 local.block_on(&runtime, async move {
773 let cpu_started = ThreadTime::now();
774 let catalog = match adapters(&lane).with_adapter(proxy_adapter) {
775 Ok(catalog) => catalog,
776 Err(error) => {
777 let _ = started.send(Err(error.to_string()));
778 return;
779 }
780 };
781 let driver = TokioDriver::with_epoch(epoch);
782 let runtime_diagnostics = RuntimeDiagnostics::new().with_invocation_probe(Rc::new(
783 LaneInvocationProbe::new(Arc::clone(&diagnostics), lane.clone()),
784 ));
785 let app = match lenso_kernel::Kernel::start_with_diagnostics(
786 plan,
787 driver,
788 catalog,
789 runtime_diagnostics,
790 )
791 .await
792 {
793 Ok(app) => app,
794 Err(error) => {
795 let _ = started.send(Err(format!("{error:?}")));
796 return;
797 }
798 };
799 let lane_runtime = LaneRuntime::new(app.clone());
800 let _ = started.send(Ok(()));
801 diagnostics.publish_lane(&lane, &app, cpu_started.elapsed());
802 let mut sample_interval = tokio::time::interval(Duration::from_millis(10));
803 let terminal_monitor = tokio::task::spawn_local(monitor_lane_failure(
804 lane.clone(),
805 app.clone(),
806 Arc::clone(&terminal),
807 ));
808 let terminal_failure = terminal.wait();
809 tokio::pin!(terminal_failure);
810
811 loop {
812 tokio::select! {
813 biased;
814 shutdown = &mut shutdown => {
815 terminal_monitor.abort();
816 match shutdown {
817 Ok(LaneShutdown { timeout, completed }) => {
818 let outcome = app.shutdown(timeout).await;
819 let _ = completed.send(outcome);
820 }
821 Err(_) => {
822 let _ = app.shutdown(Duration::from_secs(1)).await;
823 }
824 }
825 break;
826 }
827 _ = &mut terminal_failure => {
828 terminal_monitor.abort();
829 let _ = app.shutdown(Duration::from_secs(1)).await;
830 break;
831 },
832 command = commands.recv() => if let Some(task) = command {
833 task(lane_runtime.clone());
834 } else {
835 terminal_monitor.abort();
836 if !terminal.is_stopping() {
837 terminal.fail(ReplicatedRunnerError::LaneUnavailable {
838 lane: lane.to_string(),
839 });
840 }
841 let _ = app.shutdown(Duration::from_secs(1)).await;
842 break;
843 },
844 _ = sample_interval.tick() => {
845 diagnostics.publish_lane(&lane, &app, cpu_started.elapsed());
846 }
847 }
848 }
849 });
850}
851
852async fn monitor_lane_failure(
853 lane: ExecutionLaneId,
854 app: NativeApp,
855 terminal: Arc<ReplicatedTerminalState>,
856) {
857 let mut failure_interval = tokio::time::interval(Duration::from_millis(10));
858 loop {
859 failure_interval.tick().await;
860 if let Some(error) = app.terminal_failure() {
861 terminal.fail(ReplicatedRunnerError::LaneRuntimeFailure {
862 lane: lane.to_string(),
863 error,
864 });
865 return;
866 }
867 if terminal.is_failed() {
868 return;
869 }
870 }
871}
872
873#[cfg(test)]
874mod tests {
875 use std::time::Duration;
876
877 use lenso_app_plan::{AppComposition, ExecutionLaneId, ExecutionLanePlan};
878 use lenso_kernel::ExecutionAdapterCatalog;
879
880 use super::{ReplicatedNativeApp, ReplicatedRunnerError};
881
882 #[tokio::test(flavor = "current_thread")]
883 async fn one_lane_panic_makes_the_replicated_app_terminal_and_stops_its_peers() {
884 let plan = AppComposition::new(Vec::new(), Vec::new())
885 .with_execution_lanes(vec![
886 ExecutionLanePlan::new("lane-a"),
887 ExecutionLanePlan::new("lane-b"),
888 ])
889 .resolve()
890 .expect("the empty two-lane Plan should resolve");
891 let app = ReplicatedNativeApp::start(plan, |_| ExecutionAdapterCatalog::new())
892 .expect("both empty Kernel lanes should start");
893 let peer_commands = app
894 .lanes
895 .get(&ExecutionLaneId::new("lane-b"))
896 .expect("lane-b should exist")
897 .commands
898 .clone();
899 let flooding =
900 tokio::spawn(
901 async move { while peer_commands.send(Box::new(|_| {})).await.is_ok() {} },
902 );
903 app.lanes
904 .get(&ExecutionLaneId::new("lane-a"))
905 .expect("lane-a should exist")
906 .commands
907 .send(Box::new(|_| panic!("injected lane panic")))
908 .await
909 .expect("lane-a should accept the injected task");
910
911 let failure = tokio::time::timeout(Duration::from_secs(1), app.wait_for_terminal())
912 .await
913 .expect("the lane panic should become terminal promptly");
914 assert_eq!(
915 failure,
916 ReplicatedRunnerError::LanePanicked {
917 lane: "lane-a".to_owned(),
918 }
919 );
920 assert!(app.is_failed());
921 assert_eq!(app.terminal_failure(), Some(failure.clone()));
922 assert_eq!(
923 tokio::time::timeout(Duration::from_secs(1), app.shutdown(Duration::from_secs(1)))
924 .await
925 .expect("a saturated peer lane should still observe terminal failure"),
926 Err(failure)
927 );
928 flooding
929 .await
930 .expect("the peer command producer should stop when the lane closes");
931 }
932}