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    let mut future = invoke(execution_context.clone());
255    let ready = poll_fn(|cx| {
256        Poll::Ready(match future.as_mut().poll(cx) {
257            Poll::Ready(output) => Some(output),
258            Poll::Pending => None,
259        })
260    })
261    .await;
262    if let Some(output) = ready {
263        ledger.settle(id);
264        ensure_context_active(driver, context)?;
265        if generation.is_cancelled() {
266            return Err(RuntimeFailure::Unavailable { capability });
267        }
268        return Ok(output);
269    }
270    let (sender, mut receiver) = oneshot::channel();
271    let execution_driver = driver.clone();
272    let execution_generation = generation.clone();
273    (driver.spawn_local)(Box::pin(async move {
274        let output = future.await;
275        let result = ensure_context_active(&execution_driver, &execution_context).and_then(|()| {
276            if execution_generation.is_cancelled() {
277                Err(RuntimeFailure::Unavailable { capability })
278            } else {
279                Ok(output)
280            }
281        });
282        ledger.settle(id);
283        // A result accepted here remains final even if the waiter is polled later.
284        let _ = sender.send(result);
285    }))
286    .map_err(|error| RuntimeFailure::Internal {
287        detail: format!("cannot schedule execution owner: {error}"),
288    })?;
289    let mut cancelled = context.cancellation.cancelled().boxed_local();
290    let mut generation_cancelled = generation.cancelled().boxed_local();
291    let mut deadline = context.deadline().map_or_else(
292        || futures::future::pending().boxed_local(),
293        |deadline| (driver.sleep_until)(deadline),
294    );
295    poll_fn(|cx| {
296        // A previously accepted terminal result wins over subsequent cancellation.
297        if let Poll::Ready(result) = std::pin::Pin::new(&mut receiver).poll(cx) {
298            return Poll::Ready(result.unwrap_or_else(|_| {
299                Err(RuntimeFailure::Internal {
300                    detail: "execution owner ended without settlement".to_owned(),
301                })
302            }));
303        }
304        let _ = cancelled.as_mut().poll(cx);
305        let _ = deadline.as_mut().poll(cx);
306        if let Err(error) = ensure_context_active(driver, context) {
307            return Poll::Ready(Err(error));
308        }
309        if generation_cancelled.as_mut().poll(cx).is_ready() {
310            return Poll::Ready(Err(RuntimeFailure::Unavailable { capability }));
311        }
312        Poll::Pending
313    })
314    .await
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::{DeterministicDriver, RequestAdmission, RequestAdmissionPlan, RuntimeDriver};
321    use std::time::Duration;
322
323    #[test]
324    fn cancelled_waiter_retains_execution_and_capacity_until_work_really_finishes() {
325        let driver = DeterministicDriver::new();
326        let control = DriverControl::new(&driver);
327        let ledger = Rc::new(ExecutionLedger::default());
328        let cancellation = CancellationToken::new();
329        let context = InvocationContext::new(1, None, cancellation.clone());
330        let admission = RequestAdmission::new(RequestAdmissionPlan::new(0, 1));
331        let permit = admission
332            .try_acquire("test", "work", &context, &control)
333            .unwrap();
334        let (finish, work) = oneshot::channel::<()>();
335        let execution = execute(
336            ledger.clone(),
337            &control,
338            "provider",
339            &context,
340            CancellationToken::new(),
341            "test",
342            vec![permit],
343            |_| work.boxed_local(),
344        );
345        driver.run(async {
346            futures::pin_mut!(execution);
347            assert!(execution.as_mut().now_or_never().is_none());
348            cancellation.cancel();
349            assert!(matches!(
350                execution.await,
351                Err(RuntimeFailure::Cancelled { request_id: 1 })
352            ));
353        });
354        assert!(!ledger.is_settled(Some("provider")));
355        let new_context = InvocationContext::new(2, None, CancellationToken::new());
356        for _ in 0..32 {
357            assert!(matches!(
358                admission.try_acquire("test", "work", &new_context, &control),
359                Err(RuntimeFailure::ResourceExhausted { .. })
360            ));
361        }
362        assert_eq!(ledger.entries.borrow().len(), 1);
363        finish.send(()).unwrap();
364        driver.run(driver.yield_now());
365        assert!(ledger.is_settled(None));
366        assert!(
367            admission
368                .try_acquire("test", "work", &new_context, &control)
369                .is_ok()
370        );
371    }
372
373    #[test]
374    fn dropped_waiter_does_not_drop_the_execution_owner() {
375        let driver = DeterministicDriver::new();
376        let control = DriverControl::new(&driver);
377        let ledger = Rc::new(ExecutionLedger::default());
378        let context = InvocationContext::new(1, None, CancellationToken::new());
379        let (finish, work) = oneshot::channel::<()>();
380        assert!(
381            execute(
382                ledger.clone(),
383                &control,
384                "provider",
385                &context,
386                CancellationToken::new(),
387                "test",
388                vec![],
389                |_| work.boxed_local()
390            )
391            .now_or_never()
392            .is_none()
393        );
394        assert!(!ledger.is_settled(None));
395        finish.send(()).unwrap();
396        driver.run(driver.yield_now());
397        assert!(ledger.is_settled(None));
398    }
399
400    #[test]
401    fn cancellation_precedes_inclusive_deadline_and_same_poll_completion() {
402        for cancel in [false, true] {
403            let driver = DeterministicDriver::new();
404            let control = DriverControl::new(&driver);
405            let cancellation = CancellationToken::new();
406            let context =
407                InvocationContext::new(1, Some(Duration::from_secs(1)), cancellation.clone());
408            let worker_driver = driver.clone();
409            let work = async move {
410                worker_driver.advance(Duration::from_secs(1));
411                if cancel {
412                    cancellation.cancel();
413                }
414                42
415            }
416            .boxed_local();
417            let result = driver.run(execute(
418                Rc::default(),
419                &control,
420                "provider",
421                &context,
422                CancellationToken::new(),
423                "test",
424                vec![],
425                |_| work,
426            ));
427            assert_eq!(
428                result,
429                Err(if cancel {
430                    RuntimeFailure::Cancelled { request_id: 1 }
431                } else {
432                    RuntimeFailure::DeadlineExceeded { request_id: 1 }
433                })
434            );
435        }
436    }
437
438    #[test]
439    fn an_already_accepted_success_survives_later_cancellation() {
440        let driver = DeterministicDriver::new();
441        let control = DriverControl::new(&driver);
442        let cancellation = CancellationToken::new();
443        let context = InvocationContext::new(1, None, cancellation.clone());
444        let (finish, work) = oneshot::channel::<u32>();
445        let execution = execute(
446            Rc::default(),
447            &control,
448            "provider",
449            &context,
450            CancellationToken::new(),
451            "test",
452            vec![],
453            |_| work.boxed_local(),
454        );
455        futures::pin_mut!(execution);
456        assert!(execution.as_mut().now_or_never().is_none());
457        finish.send(42).unwrap();
458        driver.run(driver.yield_now());
459        cancellation.cancel();
460        assert_eq!(driver.run(execution), Ok(Ok(42)));
461    }
462}