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