Skip to main content

lenso_runner/replicated/
transfer.rs

1use std::{
2    any::Any,
3    collections::BTreeMap,
4    fmt,
5    future::Future,
6    marker::PhantomData,
7    pin::Pin,
8    rc::Rc,
9    sync::{
10        Arc,
11        atomic::{AtomicU8, Ordering},
12    },
13    task::{Context, Poll},
14    time::Instant,
15};
16
17use super::{LaneRoute, LaneTask};
18use futures::{future::LocalBoxFuture, task::AtomicWaker};
19use lenso_app_plan::ResolvedAppPlan;
20use lenso_kernel::{
21    CancellationToken, InvocationContext, NativeRequestEndpoint, NativeRequestFuture,
22    RequestCapability, RuntimeFailure, TypedNativeRequestEndpoint,
23};
24
25trait RequestTransferFactory: fmt::Debug + Send + Sync {
26    fn endpoint(&self, provider_lane: LaneRoute, epoch: Instant) -> Rc<dyn NativeRequestEndpoint>;
27}
28
29struct TypedRequestTransferFactory<C: RequestCapability> {
30    operations: &'static [&'static str],
31    capability: PhantomData<fn() -> C>,
32}
33
34impl<C: RequestCapability> fmt::Debug for TypedRequestTransferFactory<C> {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        formatter
37            .debug_struct("TypedRequestTransferFactory")
38            .field("capability", &C::ID)
39            .finish_non_exhaustive()
40    }
41}
42
43impl<C> RequestTransferFactory for TypedRequestTransferFactory<C>
44where
45    C: RequestCapability,
46    C::Request: Send,
47    C::Response: Send,
48    C::DomainError: Send,
49{
50    fn endpoint(&self, provider_lane: LaneRoute, epoch: Instant) -> Rc<dyn NativeRequestEndpoint> {
51        let typed_provider_lane = provider_lane.clone();
52        let operations = self.operations;
53        Rc::new(CrossLaneRequestEndpoint::<C> {
54            operations: self.operations,
55            typed: TypedNativeRequestEndpoint::new(move |operation, request, context| {
56                let Some(operation) = operations
57                    .iter()
58                    .copied()
59                    .find(|candidate| *candidate == operation)
60                else {
61                    return Box::pin(futures::future::ready(Err(
62                        RuntimeFailure::UnknownOperation {
63                            capability: C::ID,
64                            operation: operation.to_owned(),
65                        },
66                    )));
67                };
68                invoke_cross_lane::<C>(
69                    typed_provider_lane.clone(),
70                    epoch,
71                    operation,
72                    request,
73                    context,
74                )
75            }),
76        })
77    }
78}
79
80/// Native request types registered for zero-serialization cross-lane transfer.
81#[derive(Clone, Debug, Default)]
82pub struct CrossLaneRequestCatalog {
83    factories: BTreeMap<&'static str, Arc<dyn RequestTransferFactory>>,
84}
85
86impl CrossLaneRequestCatalog {
87    /// Creates an empty catalog.
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Registers one generated request Capability whose values are `Send`.
93    #[must_use]
94    pub fn with_request<C>(mut self, operations: &'static [&'static str]) -> Self
95    where
96        C: RequestCapability,
97        C::Request: Send,
98        C::Response: Send,
99        C::DomainError: Send,
100    {
101        self.factories.insert(
102            C::ID,
103            Arc::new(TypedRequestTransferFactory::<C> {
104                operations,
105                capability: PhantomData,
106            }),
107        );
108        self
109    }
110
111    pub(super) fn contains(&self, capability_id: &str) -> bool {
112        self.factories.contains_key(capability_id)
113    }
114
115    pub(super) fn validate_plan(
116        &self,
117        plan: &ResolvedAppPlan,
118    ) -> Result<(), super::ReplicatedRunnerError> {
119        for binding in plan.capability_bindings() {
120            let consumer = plan
121                .plugin_instance(binding.consumer_instance())
122                .expect("validated binding consumer should exist");
123            let provider = plan
124                .plugin_instance(binding.provider_instance())
125                .expect("validated binding provider should exist");
126            let endpoint = provider
127                .provided_capabilities()
128                .iter()
129                .find(|endpoint| endpoint.capability_id() == binding.capability_id())
130                .expect("validated provider endpoint should exist");
131            if consumer.execution_lane() != provider.execution_lane()
132                && !endpoint.request_operations().is_empty()
133                && !self.contains(binding.capability_id())
134            {
135                return Err(
136                    super::ReplicatedRunnerError::MissingCrossLaneRequestTransfer {
137                        capability: binding.capability_id().to_owned(),
138                    },
139                );
140            }
141        }
142        Ok(())
143    }
144
145    pub(super) fn endpoint(
146        &self,
147        capability_id: &str,
148        provider_lane: LaneRoute,
149        epoch: Instant,
150    ) -> Option<Rc<dyn NativeRequestEndpoint>> {
151        self.factories
152            .get(capability_id)
153            .map(|factory| factory.endpoint(provider_lane, epoch))
154    }
155}
156
157struct CrossLaneRequestEndpoint<C: RequestCapability> {
158    operations: &'static [&'static str],
159    typed: TypedNativeRequestEndpoint<C>,
160}
161
162impl<C: RequestCapability> fmt::Debug for CrossLaneRequestEndpoint<C> {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter
165            .debug_struct("CrossLaneRequestEndpoint")
166            .field("capability", &C::ID)
167            .finish_non_exhaustive()
168    }
169}
170
171impl<C> NativeRequestEndpoint for CrossLaneRequestEndpoint<C>
172where
173    C: RequestCapability,
174    C::Request: Send,
175    C::Response: Send,
176    C::DomainError: Send,
177{
178    fn capability_id(&self) -> &'static str {
179        C::ID
180    }
181
182    fn descriptor_version(&self) -> &'static str {
183        C::DESCRIPTOR_VERSION
184    }
185
186    fn operations(&self) -> &'static [&'static str] {
187        self.operations
188    }
189
190    fn typed_endpoint(&self) -> Option<&dyn Any> {
191        Some(&self.typed)
192    }
193
194    fn invoke(
195        &self,
196        operation: &str,
197        request: Box<dyn Any>,
198        context: InvocationContext,
199    ) -> LocalBoxFuture<'static, Result<Result<Box<dyn Any>, Box<dyn Any>>, RuntimeFailure>> {
200        let Ok(request) = request.downcast::<C::Request>() else {
201            return Box::pin(futures::future::ready(Err(
202                RuntimeFailure::ProtocolViolation { capability: C::ID },
203            )));
204        };
205        let invocation = self.typed.invoke(operation, *request, context);
206        Box::pin(async move {
207            let result = invocation.await?;
208            Ok(result
209                .map(|response| Box::new(response) as Box<dyn Any>)
210                .map_err(|error| Box::new(error) as Box<dyn Any>))
211        })
212    }
213}
214
215#[allow(clippy::too_many_lines)]
216fn invoke_cross_lane<C>(
217    provider_lane: LaneRoute,
218    epoch: Instant,
219    operation: &'static str,
220    request: C::Request,
221    context: InvocationContext,
222) -> NativeRequestFuture<C>
223where
224    C: RequestCapability,
225    C::Request: Send,
226    C::Response: Send,
227    C::DomainError: Send,
228{
229    Box::pin(async move {
230        let provider_lane = provider_lane.upgrade().ok_or_else(lane_unavailable::<C>)?;
231        let caller_instance = context
232            .caller_instance()
233            .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
234                detail: format!("cross-lane invocation of `{}` has no planned caller", C::ID),
235            })?
236            .to_owned();
237        let cancellation = context.cancellation();
238        let deadline = context.deadline();
239        let request_id = context.request_id();
240        let (transferred, cancellation_signal) = TransferredInvocationContext::capture(&context);
241        let mut cancellation_guard = TransferredCancellationGuard::new(cancellation_signal);
242        let (completed, completion) = futures::channel::oneshot::channel();
243        let command: LaneTask = Box::new(move |lane| {
244            tokio::task::spawn_local(async move {
245                let local_cancellation = CancellationToken::new();
246                let (context, transferred_cancellation) =
247                    transferred.restore(local_cancellation.clone());
248                if transferred_cancellation.is_cancelled() {
249                    local_cancellation.cancel();
250                }
251                let handle = match lane.request_handle::<C>(&caller_instance) {
252                    Ok(handle) => handle,
253                    Err(error) => {
254                        let _ = completed.send(Err(error));
255                        return;
256                    }
257                };
258                let invocation = handle.invoke_with_context(operation, context, request);
259                tokio::pin!(invocation);
260                let result = if local_cancellation.is_cancelled() {
261                    invocation.await
262                } else {
263                    tokio::select! {
264                        result = &mut invocation => result,
265                        () = transferred_cancellation.cancelled() => {
266                            local_cancellation.cancel();
267                            invocation.await
268                        }
269                    }
270                };
271                let _ = completed.send(result);
272            });
273        });
274
275        let cancelled = cancellation.cancelled();
276        tokio::pin!(cancelled);
277        let result = if let Some(deadline) = deadline {
278            let sleep = tokio::time::sleep_until((epoch + deadline).into());
279            tokio::pin!(sleep);
280            if let Err(error) = provider_lane.try_send(command) {
281                let command = error.into_inner();
282                let send = provider_lane.send(command);
283                tokio::pin!(send);
284                tokio::select! {
285                    result = &mut send => result.map_err(|_| lane_unavailable::<C>())?,
286                    () = &mut cancelled => {
287                        cancellation_guard.cancel();
288                        return Err(RuntimeFailure::Cancelled { request_id });
289                    }
290                    () = &mut sleep => {
291                        cancellation_guard.cancel();
292                        return Err(RuntimeFailure::DeadlineExceeded { request_id });
293                    }
294                }
295            }
296            tokio::select! {
297                biased;
298                result = completion => result.map_err(|_| lane_unavailable::<C>())?,
299                () = &mut cancelled => {
300                    cancellation_guard.cancel();
301                    Err(RuntimeFailure::Cancelled { request_id })
302                }
303                () = &mut sleep => {
304                    cancellation_guard.cancel();
305                    Err(RuntimeFailure::DeadlineExceeded { request_id })
306                }
307            }
308        } else {
309            if let Err(error) = provider_lane.try_send(command) {
310                let send = provider_lane.send(error.into_inner());
311                tokio::pin!(send);
312                tokio::select! {
313                    result = &mut send => result.map_err(|_| lane_unavailable::<C>())?,
314                    () = &mut cancelled => {
315                        cancellation_guard.cancel();
316                        return Err(RuntimeFailure::Cancelled { request_id });
317                    }
318                }
319            }
320            tokio::select! {
321                biased;
322                result = completion => result.map_err(|_| lane_unavailable::<C>())?,
323                () = &mut cancelled => {
324                    cancellation_guard.cancel();
325                    Err(RuntimeFailure::Cancelled { request_id })
326                }
327            }
328        };
329        cancellation_guard.disarm();
330        result
331    })
332}
333
334fn lane_unavailable<C: RequestCapability>() -> RuntimeFailure {
335    RuntimeFailure::Internal {
336        detail: format!("provider lane for `{}` is unavailable", C::ID),
337    }
338}
339
340#[derive(Debug)]
341pub(super) struct TransferredInvocationContext {
342    request_id: u64,
343    deadline: Option<std::time::Duration>,
344    cancellation: Arc<TransferredCancellation>,
345    extensions: Vec<lenso_kernel::InvocationExtension>,
346    sealed_extensions: Vec<lenso_kernel::SealedInvocationExtension>,
347}
348
349impl TransferredInvocationContext {
350    pub(super) fn capture(context: &InvocationContext) -> (Self, Arc<TransferredCancellation>) {
351        let cancellation = Arc::new(TransferredCancellation::new(context.is_cancelled()));
352        (
353            Self {
354                request_id: context.request_id(),
355                deadline: context.deadline(),
356                cancellation: Arc::clone(&cancellation),
357                extensions: context.extensions().cloned().collect(),
358                sealed_extensions: context.sealed_extensions().cloned().collect(),
359            },
360            cancellation,
361        )
362    }
363
364    pub(super) fn restore(
365        self,
366        cancellation: CancellationToken,
367    ) -> (InvocationContext, Arc<TransferredCancellation>) {
368        let mut context = InvocationContext::new(self.request_id, self.deadline, cancellation);
369        for extension in self.extensions {
370            context = context
371                .with_extension(extension.key(), extension.value().to_vec())
372                .expect("captured ordinary Invocation Context extension remains valid");
373        }
374        for extension in self.sealed_extensions {
375            context = context
376                .with_sealed_extension(extension)
377                .expect("captured sealed Invocation Context extension remains valid");
378        }
379        (context, self.cancellation)
380    }
381}
382
383#[derive(Debug)]
384pub(super) struct TransferredCancellation {
385    // A transferred invocation has exactly one provider-side waiter.
386    state: AtomicU8,
387    waker: AtomicWaker,
388}
389
390const TRANSFER_PENDING: u8 = 0;
391const TRANSFER_CANCELLED: u8 = 1;
392const TRANSFER_ACCEPTED: u8 = 2;
393
394/// Ensures dropping a caller-side transfer future wakes its provider-side cancellation waiter.
395#[derive(Debug)]
396pub(super) struct TransferredCancellationGuard {
397    cancellation: Arc<TransferredCancellation>,
398    armed: bool,
399}
400
401impl TransferredCancellationGuard {
402    pub(super) fn new(cancellation: Arc<TransferredCancellation>) -> Self {
403        Self {
404            cancellation,
405            armed: true,
406        }
407    }
408
409    pub(super) fn cancellation(&self) -> Arc<TransferredCancellation> {
410        Arc::clone(&self.cancellation)
411    }
412
413    pub(super) fn cancel(&self) {
414        self.cancellation.cancel();
415    }
416
417    pub(super) fn disarm(&mut self) {
418        self.armed = false;
419    }
420}
421
422impl Drop for TransferredCancellationGuard {
423    fn drop(&mut self) {
424        if self.armed {
425            self.cancellation.cancel();
426        }
427    }
428}
429
430impl TransferredCancellation {
431    fn new(cancelled: bool) -> Self {
432        Self {
433            state: AtomicU8::new(if cancelled {
434                TRANSFER_CANCELLED
435            } else {
436                TRANSFER_PENDING
437            }),
438            waker: AtomicWaker::new(),
439        }
440    }
441
442    pub(super) fn is_cancelled(&self) -> bool {
443        self.state.load(Ordering::Acquire) == TRANSFER_CANCELLED
444    }
445
446    pub(super) fn cancel(&self) {
447        if self.state.swap(TRANSFER_CANCELLED, Ordering::AcqRel) != TRANSFER_CANCELLED {
448            self.waker.wake();
449        }
450    }
451
452    pub(super) fn accept(&self) {
453        if self
454            .state
455            .compare_exchange(
456                TRANSFER_PENDING,
457                TRANSFER_ACCEPTED,
458                Ordering::AcqRel,
459                Ordering::Acquire,
460            )
461            .is_ok()
462        {
463            self.waker.wake();
464        }
465    }
466
467    pub(super) fn cancelled(&self) -> TransferredCancellationFuture<'_> {
468        TransferredCancellationFuture { cancellation: self }
469    }
470
471    pub(super) fn settled(&self) -> TransferredSettlementFuture<'_> {
472        TransferredSettlementFuture { cancellation: self }
473    }
474}
475
476pub(super) struct TransferredCancellationFuture<'a> {
477    cancellation: &'a TransferredCancellation,
478}
479
480pub(super) struct TransferredSettlementFuture<'a> {
481    cancellation: &'a TransferredCancellation,
482}
483
484impl Future for TransferredSettlementFuture<'_> {
485    type Output = bool;
486
487    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
488        let state = self.cancellation.state.load(Ordering::Acquire);
489        if state != TRANSFER_PENDING {
490            return Poll::Ready(state == TRANSFER_CANCELLED);
491        }
492        self.cancellation.waker.register(context.waker());
493        let state = self.cancellation.state.load(Ordering::Acquire);
494        if state == TRANSFER_PENDING {
495            Poll::Pending
496        } else {
497            Poll::Ready(state == TRANSFER_CANCELLED)
498        }
499    }
500}
501
502impl Future for TransferredCancellationFuture<'_> {
503    type Output = ();
504
505    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
506        if self.cancellation.is_cancelled() {
507            return Poll::Ready(());
508        }
509        self.cancellation.waker.register(context.waker());
510        if self.cancellation.is_cancelled() {
511            Poll::Ready(())
512        } else {
513            Poll::Pending
514        }
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use std::sync::Arc;
521
522    use super::TransferredCancellation;
523
524    #[tokio::test(flavor = "current_thread")]
525    async fn transferred_cancellation_observes_an_initial_signal() {
526        let cancellation = TransferredCancellation::new(true);
527
528        cancellation.cancelled().await;
529
530        assert!(cancellation.is_cancelled());
531    }
532
533    #[tokio::test(flavor = "current_thread")]
534    async fn transferred_cancellation_wakes_the_provider_waiter() {
535        let cancellation = Arc::new(TransferredCancellation::new(false));
536        let canceller = Arc::clone(&cancellation);
537
538        tokio::join!(cancellation.cancelled(), async move {
539            tokio::task::yield_now().await;
540            canceller.cancel();
541        });
542
543        assert!(cancellation.is_cancelled());
544    }
545
546    #[tokio::test(flavor = "current_thread")]
547    async fn transferred_acceptance_settles_without_cancellation() {
548        let cancellation = Arc::new(TransferredCancellation::new(false));
549        let accepter = Arc::clone(&cancellation);
550
551        let cancelled = tokio::join!(cancellation.settled(), async move {
552            tokio::task::yield_now().await;
553            accepter.accept();
554        })
555        .0;
556
557        assert!(!cancelled);
558        assert!(!cancellation.is_cancelled());
559    }
560}