Skip to main content

lenso_kernel/
settlement.rs

1//! Execution ownership outlives caller interest and never guesses termination.
2
3use super::driver::RequestPermit;
4use super::{
5    CancellationToken, DriverControl, InvocationContext, LocalBoxFuture, RuntimeFailure,
6    ensure_context_active,
7};
8use futures::{FutureExt, channel::oneshot};
9use std::{
10    cell::{Cell, RefCell},
11    collections::BTreeMap,
12    future::{Future, poll_fn},
13    rc::Rc,
14    task::Poll,
15};
16
17#[derive(Default, Debug)]
18pub(super) struct ExecutionLedger {
19    next_id: Cell<u64>,
20    entries: RefCell<BTreeMap<u64, ExecutionEntry>>,
21    provider_admissions: RefCell<BTreeMap<(String, String, String), super::RequestAdmission>>,
22}
23
24#[allow(
25    clippy::too_many_arguments,
26    reason = "request execution carries explicit admission and generation context"
27)]
28pub(super) async fn request<T: 'static>(
29    runtime: &Rc<super::NativeAppRuntime>,
30    provider: &str,
31    operation: &str,
32    context: &InvocationContext,
33    generation: CancellationToken,
34    capability: &'static str,
35    permit: RequestPermit,
36    invoke: impl FnOnce(InvocationContext) -> LocalBoxFuture<'static, Result<T, RuntimeFailure>>,
37) -> Result<Result<T, RuntimeFailure>, RuntimeFailure> {
38    let instance = runtime
39        .plan
40        .plugin_instance(provider)
41        .expect("prepared endpoint has a planned provider");
42    let named_caller = context
43        .caller_instance()
44        .and_then(|caller| runtime.plan.plugin_instance(caller))
45        .is_some_and(|caller| caller.authoring_version() == 2);
46    if instance.authoring_version() == 1 && !named_caller {
47        let output = super::await_with_generation_context(
48            &runtime.driver,
49            context,
50            generation,
51            capability,
52            invoke(context.clone()),
53        )
54        .await?;
55        drop(permit);
56        return Ok(output.map_err(|error| {
57            super::schedule_plugin_supervision_after_failure(runtime, provider, error)
58        }));
59    }
60    let limits = instance
61        .provided_capabilities()
62        .iter()
63        .find(|endpoint| endpoint.capability_id() == capability)
64        .and_then(|endpoint| endpoint.operation_admission(operation))
65        .unwrap_or_default();
66    let aggregate = runtime
67        .executions
68        .provider_admissions
69        .borrow_mut()
70        .entry((
71            provider.to_owned(),
72            capability.to_owned(),
73            operation.to_owned(),
74        ))
75        .or_insert_with(|| super::RequestAdmission::new(limits))
76        .clone();
77    let provider_permit = aggregate
78        .acquire(capability, operation, context, &runtime.driver)
79        .await?;
80    if instance.authoring_version() == 1 {
81        let permits = (permit, provider_permit);
82        let output = super::await_with_generation_context(
83            &runtime.driver,
84            context,
85            generation,
86            capability,
87            invoke(context.clone()),
88        )
89        .await?;
90        drop(permits);
91        return Ok(output.map_err(|error| {
92            super::schedule_plugin_supervision_after_failure(runtime, provider, error)
93        }));
94    }
95    execute(
96        runtime.executions.clone(),
97        &runtime.driver,
98        provider,
99        context,
100        generation,
101        capability,
102        vec![permit, provider_permit],
103        invoke,
104        failure_observer(runtime, provider),
105    )
106    .await
107}
108
109/// Executes one non-request Adapter operation under the same Driver-owned
110/// settlement rules as authoring-version-2 requests.
111pub(super) async fn operation<T: 'static>(
112    runtime: &Rc<super::NativeAppRuntime>,
113    provider: &str,
114    context: &InvocationContext,
115    generation: CancellationToken,
116    capability: &'static str,
117    invoke: impl FnOnce(InvocationContext) -> LocalBoxFuture<'static, Result<T, RuntimeFailure>>,
118) -> Result<Result<T, RuntimeFailure>, RuntimeFailure> {
119    let instance = runtime
120        .plan
121        .plugin_instance(provider)
122        .expect("prepared endpoint has a planned provider");
123    if instance.authoring_version() == 1 {
124        let output = super::await_with_generation_context(
125            &runtime.driver,
126            context,
127            generation,
128            capability,
129            invoke(context.clone()),
130        )
131        .await?;
132        return Ok(output.map_err(|error| {
133            super::schedule_plugin_supervision_after_failure(runtime, provider, error)
134        }));
135    }
136    execute(
137        runtime.executions.clone(),
138        &runtime.driver,
139        provider,
140        context,
141        generation,
142        capability,
143        vec![],
144        invoke,
145        failure_observer(runtime, provider),
146    )
147    .await
148}
149
150fn failure_observer<T>(
151    runtime: &Rc<super::NativeAppRuntime>,
152    provider: &str,
153) -> impl FnOnce(&mut Result<T, RuntimeFailure>) + 'static {
154    let generation = runtime.supervision.borrow()[provider].generation;
155    let runtime = Rc::downgrade(runtime);
156    let provider = provider.to_owned();
157    move |output| {
158        let Err(error) = output else { return };
159        let Some(runtime) = runtime.upgrade() else {
160            return;
161        };
162        // The execution owner observes once, for the generation it dispatched to.
163        // A caller polling its accepted result later must not supervise again.
164        if runtime.supervision.borrow()[&provider].generation == generation {
165            *error = super::schedule_plugin_supervision_after_failure(
166                &runtime,
167                &provider,
168                error.clone(),
169            );
170        }
171    }
172}
173
174#[derive(Debug)]
175struct ExecutionEntry {
176    outstanding: usize,
177    provider: String,
178    // Only observed execution completion removes the entry and releases these.
179    // A Driver dropping a Future leaves the entry uncertain and capacity held.
180    _permits: Vec<RequestPermit>,
181}
182
183impl ExecutionLedger {
184    pub(super) fn is_settled(&self, provider: Option<&str>) -> bool {
185        !self
186            .entries
187            .borrow()
188            .values()
189            .any(|entry| provider.is_none_or(|provider| entry.provider == provider))
190    }
191
192    fn admit(&self, provider: &str, permits: Vec<RequestPermit>) -> Result<u64, RuntimeFailure> {
193        let mut entries = self.entries.borrow_mut();
194        let mut candidate = self.next_id.get();
195        let mut available = None;
196        for _ in 0..=entries.len() {
197            if !entries.contains_key(&candidate) {
198                available = Some(candidate);
199                break;
200            }
201            candidate = candidate.wrapping_add(1);
202        }
203        let id = available.ok_or(RuntimeFailure::AdmissionClosed)?;
204        self.next_id.set(id.wrapping_add(1));
205        entries.insert(
206            id,
207            ExecutionEntry {
208                outstanding: 1,
209                provider: provider.to_owned(),
210                _permits: permits,
211            },
212        );
213        Ok(id)
214    }
215
216    fn settle(&self, id: u64) {
217        let mut entries = self.entries.borrow_mut();
218        if let Some(entry) = entries.get_mut(&id) {
219            entry.outstanding -= 1;
220            if entry.outstanding == 0 {
221                entries.remove(&id);
222            }
223        }
224    }
225}
226
227/// Proof that Adapter-managed work has actually ended. Dropping the token,
228/// acknowledging cancellation, or disconnecting does not settle execution.
229#[derive(Debug)]
230#[must_use = "call settle only after retained execution has actually terminated"]
231pub struct ExecutionLease {
232    scope: ExecutionScope,
233}
234
235impl ExecutionLease {
236    /// Reports observed termination after retained resources are safe.
237    pub fn settle(self) {
238        self.scope.ledger.settle(self.scope.id);
239    }
240}
241
242#[derive(Clone, Debug)]
243pub(crate) struct ExecutionScope {
244    ledger: Rc<ExecutionLedger>,
245    id: u64,
246}
247
248impl ExecutionScope {
249    pub(crate) fn retain(&self) -> Result<ExecutionLease, RuntimeFailure> {
250        let mut entries = self.ledger.entries.borrow_mut();
251        let entry = entries
252            .get_mut(&self.id)
253            .ok_or(RuntimeFailure::AdmissionClosed)?;
254        entry.outstanding = entry
255            .outstanding
256            .checked_add(1)
257            .ok_or(RuntimeFailure::AdmissionClosed)?;
258        Ok(ExecutionLease {
259            scope: self.clone(),
260        })
261    }
262}
263
264/// The fast path polls inline; pending work is transferred to the Driver before
265/// this Future can yield. Dropping the caller never drops its execution owner.
266#[allow(
267    clippy::too_many_arguments,
268    reason = "explicit execution ownership transfer"
269)]
270pub(super) async fn execute<T: 'static>(
271    ledger: Rc<ExecutionLedger>,
272    driver: &DriverControl,
273    provider: &str,
274    context: &InvocationContext,
275    generation: CancellationToken,
276    capability: &'static str,
277    permits: Vec<RequestPermit>,
278    invoke: impl FnOnce(InvocationContext) -> LocalBoxFuture<'static, T>,
279    observe: impl FnOnce(&mut T) + 'static,
280) -> Result<T, RuntimeFailure> {
281    ensure_context_active(driver, context)?;
282    if generation.is_cancelled() {
283        return Err(RuntimeFailure::Unavailable { capability });
284    }
285    let id = ledger.admit(provider, permits)?;
286    let mut execution_context = context.clone();
287    execution_context.execution = Some(ExecutionScope {
288        ledger: ledger.clone(),
289        id,
290    });
291    execution_context.remaining_budget = context
292        .deadline()
293        .map(|deadline| deadline.saturating_sub((driver.now)()));
294    let mut future = invoke(execution_context.clone());
295    let ready = poll_fn(|cx| {
296        Poll::Ready(match future.as_mut().poll(cx) {
297            Poll::Ready(output) => Some(output),
298            Poll::Pending => None,
299        })
300    })
301    .await;
302    if let Some(mut output) = ready {
303        let accepted = completion_status(driver, context, &generation, capability);
304        observe(&mut output);
305        ledger.settle(id);
306        return accepted.map(|()| output);
307    }
308    let (sender, mut receiver) = oneshot::channel();
309    let execution_driver = driver.clone();
310    let execution_generation = generation.clone();
311    (driver.spawn_local)(Box::pin(async move {
312        let mut output = future.await;
313        // Freeze caller delivery before supervision cancels this generation, but
314        // observe the original provider result even if the caller has cancelled.
315        let accepted = completion_status(
316            &execution_driver,
317            &execution_context,
318            &execution_generation,
319            capability,
320        );
321        observe(&mut output);
322        let result = accepted.map(|()| output);
323        ledger.settle(id);
324        // A result accepted here remains final even if the waiter is polled later.
325        let _ = sender.send(result);
326    }))
327    .map_err(|error| RuntimeFailure::Internal {
328        detail: format!("cannot schedule execution owner: {error}"),
329    })?;
330    let mut cancelled = context.cancellation.cancelled().boxed_local();
331    let mut generation_cancelled = generation.cancelled().boxed_local();
332    let mut deadline = context.deadline().map_or_else(
333        || futures::future::pending().boxed_local(),
334        |deadline| (driver.sleep_until)(deadline),
335    );
336    poll_fn(|cx| {
337        // A previously accepted terminal result wins over subsequent cancellation.
338        if let Poll::Ready(result) = std::pin::Pin::new(&mut receiver).poll(cx) {
339            return Poll::Ready(result.unwrap_or_else(|_| {
340                Err(RuntimeFailure::Internal {
341                    detail: "execution owner ended without settlement".to_owned(),
342                })
343            }));
344        }
345        let _ = cancelled.as_mut().poll(cx);
346        let _ = deadline.as_mut().poll(cx);
347        if let Err(error) = ensure_context_active(driver, context) {
348            return Poll::Ready(Err(error));
349        }
350        if generation_cancelled.as_mut().poll(cx).is_ready() {
351            return Poll::Ready(Err(RuntimeFailure::Unavailable { capability }));
352        }
353        Poll::Pending
354    })
355    .await
356}
357
358fn completion_status(
359    driver: &DriverControl,
360    context: &InvocationContext,
361    generation: &CancellationToken,
362    capability: &'static str,
363) -> Result<(), RuntimeFailure> {
364    ensure_context_active(driver, context)?;
365    if generation.is_cancelled() {
366        return Err(RuntimeFailure::Unavailable { capability });
367    }
368    Ok(())
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::{DeterministicDriver, RequestAdmission, RequestAdmissionPlan, RuntimeDriver};
375    use std::time::Duration;
376
377    #[test]
378    fn cancelled_waiter_retains_execution_and_capacity_until_work_really_finishes() {
379        let driver = DeterministicDriver::new();
380        let control = DriverControl::new(&driver);
381        let ledger = Rc::new(ExecutionLedger::default());
382        let cancellation = CancellationToken::new();
383        let context = InvocationContext::new(1, None, cancellation.clone());
384        let admission = RequestAdmission::new(RequestAdmissionPlan::new(0, 1));
385        let permit = admission
386            .try_acquire("test", "work", &context, &control)
387            .unwrap();
388        let (finish, work) = oneshot::channel::<()>();
389        let execution = execute(
390            ledger.clone(),
391            &control,
392            "provider",
393            &context,
394            CancellationToken::new(),
395            "test",
396            vec![permit],
397            |_| work.boxed_local(),
398            |_| {},
399        );
400        driver.run(async {
401            futures::pin_mut!(execution);
402            assert!(execution.as_mut().now_or_never().is_none());
403            cancellation.cancel();
404            assert!(matches!(
405                execution.await,
406                Err(RuntimeFailure::Cancelled { request_id: 1 })
407            ));
408        });
409        assert!(!ledger.is_settled(Some("provider")));
410        let new_context = InvocationContext::new(2, None, CancellationToken::new());
411        for _ in 0..32 {
412            assert!(matches!(
413                admission.try_acquire("test", "work", &new_context, &control),
414                Err(RuntimeFailure::ResourceExhausted { .. })
415            ));
416        }
417        assert_eq!(ledger.entries.borrow().len(), 1);
418        finish.send(()).unwrap();
419        driver.run(driver.yield_now());
420        assert!(ledger.is_settled(None));
421        assert!(
422            admission
423                .try_acquire("test", "work", &new_context, &control)
424                .is_ok()
425        );
426    }
427
428    #[test]
429    fn dropped_waiter_does_not_drop_the_execution_owner() {
430        let driver = DeterministicDriver::new();
431        let control = DriverControl::new(&driver);
432        let ledger = Rc::new(ExecutionLedger::default());
433        let context = InvocationContext::new(1, None, CancellationToken::new());
434        let (finish, work) = oneshot::channel::<()>();
435        assert!(
436            execute(
437                ledger.clone(),
438                &control,
439                "provider",
440                &context,
441                CancellationToken::new(),
442                "test",
443                vec![],
444                |_| work.boxed_local(),
445                |_| {},
446            )
447            .now_or_never()
448            .is_none()
449        );
450        assert!(!ledger.is_settled(None));
451        finish.send(()).unwrap();
452        driver.run(driver.yield_now());
453        assert!(ledger.is_settled(None));
454    }
455
456    #[test]
457    fn cancellation_precedes_inclusive_deadline_and_same_poll_completion() {
458        for cancel in [false, true] {
459            let driver = DeterministicDriver::new();
460            let control = DriverControl::new(&driver);
461            let cancellation = CancellationToken::new();
462            let context =
463                InvocationContext::new(1, Some(Duration::from_secs(1)), cancellation.clone());
464            let worker_driver = driver.clone();
465            let work = async move {
466                worker_driver.advance(Duration::from_secs(1));
467                if cancel {
468                    cancellation.cancel();
469                }
470                42
471            }
472            .boxed_local();
473            let result = driver.run(execute(
474                Rc::default(),
475                &control,
476                "provider",
477                &context,
478                CancellationToken::new(),
479                "test",
480                vec![],
481                |_| work,
482                |_| {},
483            ));
484            assert_eq!(
485                result,
486                Err(if cancel {
487                    RuntimeFailure::Cancelled { request_id: 1 }
488                } else {
489                    RuntimeFailure::DeadlineExceeded { request_id: 1 }
490                })
491            );
492        }
493    }
494
495    #[test]
496    fn provider_context_carries_a_relative_dispatch_budget() {
497        let driver = DeterministicDriver::new();
498        let control = DriverControl::new(&driver);
499        driver.advance(Duration::from_millis(250));
500        let context =
501            InvocationContext::new(1, Some(Duration::from_secs(1)), CancellationToken::new());
502
503        let remaining = driver
504            .run(execute(
505                Rc::default(),
506                &control,
507                "provider",
508                &context,
509                CancellationToken::new(),
510                "test",
511                vec![],
512                |context| futures::future::ready(context.remaining_budget()).boxed_local(),
513                |_| {},
514            ))
515            .unwrap();
516
517        assert_eq!(remaining, Some(Duration::from_millis(750)));
518        assert_eq!(context.remaining_budget(), None);
519    }
520
521    #[test]
522    fn an_already_accepted_success_survives_later_cancellation() {
523        let driver = DeterministicDriver::new();
524        let control = DriverControl::new(&driver);
525        let cancellation = CancellationToken::new();
526        let context = InvocationContext::new(1, None, cancellation.clone());
527        let (finish, work) = oneshot::channel::<u32>();
528        let execution = execute(
529            Rc::default(),
530            &control,
531            "provider",
532            &context,
533            CancellationToken::new(),
534            "test",
535            vec![],
536            |_| work.boxed_local(),
537            |_| {},
538        );
539        futures::pin_mut!(execution);
540        assert!(execution.as_mut().now_or_never().is_none());
541        finish.send(42).unwrap();
542        driver.run(driver.yield_now());
543        cancellation.cancel();
544        assert_eq!(driver.run(execution), Ok(Ok(42)));
545    }
546}