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