Skip to main content

harmos_runtime/operations/
handler.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14pub use harmos_domain::operations::instance::Id as OperationInstanceId;
15
16/* === Imports === */
17
18// Standard Library Imports
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::{
22    Arc,
23    atomic::{AtomicU64, Ordering},
24};
25use std::task::{Context as TaskContext, Poll};
26// Third Party Imports
27use tokio::sync::oneshot;
28
29// Local Imports
30use super::Manager;
31use super::admission::Admission;
32use super::caller::{Origin, Source};
33use crate::operations::OperationDeclaration;
34use crate::{Error, Result, operations::Scope, state::Position};
35
36/* === Constants === */
37
38static NEXT_INSTANCE: AtomicU64 = AtomicU64::new(1);
39static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
40
41/* === Macros === */
42
43/* === Type Aliases === */
44
45/* === Structs === */
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub struct OperationGeneration(u64);
49
50// Dropping an instance before its outcome is delivered requests cancellation;
51// `detach` keeps the operation running without a handle. A resident handle
52// (an observer or service activation) holds one lease on its shared instance:
53// dropping it releases that lease and the last release stops the instance.
54#[must_use = "dropping an unsettled operation instance requests its cancellation"]
55pub struct Running<O: OperationDeclaration> {
56    pub(crate) id: OperationInstanceId,
57    pub(super) generation: OperationGeneration,
58    pub(crate) outcome: Outcome<O::Response>,
59    pub(super) control: Arc<crate::supervision::instance::Instance>,
60    pub(super) released: bool,
61}
62
63pub(crate) enum Outcome<T: Send + Sync + 'static> {
64    Request(oneshot::Receiver<Result<crate::operations::Response<T>>>),
65    Resident {
66        lease: Option<super::resident::Lease>,
67        // Polled only through `&mut`; the lock makes a shared handle `Sync`.
68        settled: std::sync::Mutex<Settling<T>>,
69    },
70}
71
72pub(crate) type Settling<T> = Pin<Box<dyn Future<Output = Result<crate::operations::Response<T>>> + Send>>;
73
74#[derive(Clone)]
75pub struct Context<'a> {
76    pub(crate) id: OperationInstanceId,
77    pub(crate) attempt: usize,
78    pub(crate) generation: OperationGeneration,
79    pub(crate) scope: Scope<'a>,
80    pub(crate) operations: Manager,
81    pub(super) admission: Admission,
82    pub(super) request_claims: Option<super::RequestClaims>,
83    pub(super) source: Source,
84    pub(super) origin: Origin,
85
86    #[cfg(feature = "plugin-sidecar")]
87    pub(super) retirement: bool,
88}
89
90#[derive(Clone)]
91pub struct HostContext {
92    request_claims: Option<super::RequestClaims>,
93    scope: super::scope::ScopeCore,
94    id: OperationInstanceId,
95    attempt: usize,
96    generation: OperationGeneration,
97    operations: Manager,
98    admission: Admission,
99    source: Source,
100    origin: Origin,
101
102    #[cfg(feature = "plugin-sidecar")]
103    retirement: bool,
104}
105
106/* === Enums === */
107
108/* === Traits === */
109
110/* === Implementations === */
111
112impl OperationGeneration {
113    pub(super) fn fresh() -> Result<Self> {
114        NEXT_GENERATION.try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)).map(Self).map_err(|_| {
115            Error::ResourceExhausted {
116                resource: "operation provider generations",
117            }
118        })
119    }
120
121    pub fn value(self) -> u64 {
122        self.0
123    }
124}
125
126impl<O: OperationDeclaration> Running<O> {
127    pub fn id(&self) -> OperationInstanceId {
128        self.id
129    }
130
131    pub fn generation(&self) -> OperationGeneration {
132        self.generation
133    }
134
135    // A resident instance stops for every lease holder.
136    pub fn cancel(&self) {
137        match &self.outcome {
138            Outcome::Resident { lease: Some(lease), .. } => lease.stop(),
139            _ => self.control.cancel(),
140        }
141    }
142
143    pub fn status(&self) -> super::OperationStatus {
144        self.control.status()
145    }
146
147    pub fn subscribe(&self) -> tokio::sync::watch::Receiver<super::OperationStatus> {
148        self.control.settlement()
149    }
150
151    // Wait until the instance runs; a resident that stops or fails first
152    // reports why once its cleanup settles.
153    pub async fn ready(&self) -> Result<()> {
154        let mut status = self.control.settlement();
155        loop {
156            let current = status.borrow_and_update().clone();
157            match current.phase() {
158                super::OperationPhase::Running => return Ok(()),
159                phase if phase.is_terminal() || current.cancellation_requested() || current.outcome().is_some() => {
160                    self.settle().await?;
161                    return Err(Error::NotReady { domain: "operation activation" });
162                }
163                _ => {}
164            }
165            if status.changed().await.is_err() {
166                return Err(Error::NotReady { domain: "operation activation" });
167            }
168        }
169    }
170
171    #[tracing::instrument(target = "harmos::operations", name = "operations.join", level = "debug", skip_all)]
172    pub async fn join(self) -> Result<crate::operations::Response<O::Response>> {
173        self.await
174    }
175
176    pub async fn cancel_and_join(self) -> Result<crate::operations::Response<O::Response>> {
177        self.cancel();
178        self.await
179    }
180
181    // Keep the operation running without a handle until its submitting
182    // attempt ends, which cancels and settles it like any other child. A
183    // detached resident lease is held until its resources retire.
184    pub fn detach(mut self) -> OperationInstanceId {
185        self.released = true;
186        if let Outcome::Resident { lease, .. } = &mut self.outcome
187            && let Some(lease) = lease.take()
188        {
189            lease.detach();
190        }
191        self.id
192    }
193
194    // Release this handle. A resident lease that was the last one stops its
195    // instance and waits for it; a request-scoped instance is cancelled and
196    // settled.
197    pub async fn release(mut self) -> Result<()> {
198        self.released = true;
199        let lease = match &mut self.outcome {
200            Outcome::Resident { lease, .. } => lease.take(),
201            Outcome::Request(_) => None,
202        };
203        match lease {
204            Some(lease) => lease.release().await,
205            None => {
206                self.control.cancel();
207                self.settle().await
208            }
209        }
210    }
211
212    // Wait for the instance to settle without consuming the handle. A
213    // resident reports its outcome and retries a retained cleanup.
214    #[tracing::instrument(target = "harmos::operations", name = "operations.settle", level = "debug", skip_all)]
215    pub async fn settle(&self) -> Result<()> {
216        if let Outcome::Resident { lease: Some(lease), .. } = &self.outcome {
217            return lease.settlement().await;
218        }
219        self.control.settle().await.map_err(|source| Error::OperationSettlement { source })
220    }
221}
222
223impl<O: OperationDeclaration> Running<O> {
224    // Encoded invocations keep their own contract: dropping one never
225    // cancels, so the typed instance they await must not cancel either.
226    pub(super) fn unguarded(mut self) -> Self {
227        self.released = true;
228        self
229    }
230}
231
232impl<O: OperationDeclaration> Future for Running<O> {
233    type Output = Result<crate::operations::Response<O::Response>>;
234    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
235        let instance = self.get_mut();
236        let answer = match &mut instance.outcome {
237            Outcome::Request(outcome) => match Pin::new(outcome).poll(cx) {
238                Poll::Ready(Ok(answer)) => answer,
239                Poll::Ready(Err(_)) => Err(crate::Refusal::Unavailable.into()),
240                Poll::Pending => return Poll::Pending,
241            },
242            Outcome::Resident { settled, .. } => match settled.get_mut().expect("resident settlement is healthy").as_mut().poll(cx) {
243                Poll::Ready(answer) => answer,
244                Poll::Pending => return Poll::Pending,
245            },
246        };
247        instance.released = true;
248        Poll::Ready(answer)
249    }
250}
251
252impl<O: OperationDeclaration> Drop for Running<O> {
253    fn drop(&mut self) {
254        // A resident lease releases itself when dropped.
255        if matches!(self.outcome, Outcome::Request(_)) && !self.released && !self.control.is_settled() {
256            self.control.cancel();
257        }
258    }
259}
260
261impl<O> harmos_sdk::operations::Instance<O> for Running<O>
262where
263    O: OperationDeclaration,
264{
265    type Error = Error;
266
267    fn id(&self) -> OperationInstanceId {
268        self.id
269    }
270
271    fn status(&self) -> impl Future<Output = Result<harmos_domain::operations::instance::Status>> + Send {
272        std::future::ready(Ok(self.control.status().portable()))
273    }
274
275    fn cancel(&self) -> impl Future<Output = Result<()>> + Send {
276        Running::cancel(self);
277        std::future::ready(Ok(()))
278    }
279
280    fn join(self) -> impl Future<Output = Result<crate::operations::Response<O::Response>>> + Send {
281        self
282    }
283
284    fn detach(self) -> OperationInstanceId {
285        Running::detach(self)
286    }
287}
288
289impl Context<'_> {
290    pub(crate) fn owned(&self) -> HostContext {
291        HostContext {
292            request_claims: self.request_claims.clone(),
293            scope: self.scope.owned(),
294            id: self.id,
295            attempt: self.attempt,
296            generation: self.generation,
297            operations: self.operations.clone(),
298            admission: self.admission.clone(),
299            source: self.source.clone(),
300            origin: self.origin.clone(),
301            #[cfg(feature = "plugin-sidecar")]
302            retirement: self.retirement,
303        }
304    }
305
306    pub fn context(&self) -> &super::CallContext {
307        self.operations.call_context()
308    }
309
310    // Derive a child caller while retaining this attempt's identity, admission,
311    // cancellation and deadline. Only the selected portable value changes.
312    pub fn with_value<T: super::ContextValue>(&self, value: T) -> Result<Self> {
313        let context = self.context().with_value(value)?;
314        let mut child = self.clone();
315        child.operations = child.operations.with_context(context);
316        Ok(child)
317    }
318
319    pub fn id(&self) -> OperationInstanceId {
320        self.id
321    }
322
323    // An active instance this attempt's caller submitted; others stay hidden.
324    pub(crate) fn owned_instance(&self, id: OperationInstanceId) -> Option<Arc<crate::supervision::instance::Instance>> {
325        self.operations.instances.owned(id, &self.origin.identity)
326    }
327
328    pub fn attempt(&self) -> usize {
329        self.attempt
330    }
331
332    pub fn scope(&self) -> &Scope<'_> {
333        &self.scope
334    }
335
336    pub fn is_host_call(&self) -> bool {
337        self.origin.identity == super::caller::Identity::Host
338    }
339
340    // Revalidate The Caller Before Every Streamed Destination
341    pub(crate) fn admit_stream_grant(&self, grant: &harmos_domain::plugin::Grant) -> Result<()> {
342        self.source.check()?;
343        match &self.origin.grants {
344            Some(authority) => authority.admit(Some(grant)),
345            None if self.is_host_call() => Ok(()),
346            None => Err(Error::PermissionDenied),
347        }
348    }
349
350    // Refuse a provider-prepared grant the admitted caller does not hold.
351    #[cfg(any(all(feature = "plugin-guest", not(target_arch = "wasm32")), feature = "plugin-sidecar"))]
352    pub(super) fn admit_grant(&self, grant: Option<&harmos_domain::plugin::Grant>) -> Result<()> {
353        match (&self.origin.grants, grant) {
354            (Some(authority), grant) => authority.admit(grant),
355            (None, None) => Ok(()),
356            (None, Some(_)) => Err(Error::PermissionDenied),
357        }
358    }
359
360    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
361    pub fn caller_scope(&self) -> Option<&crate::plugin::PluginScope> {
362        self.origin.scope.as_ref()
363    }
364
365    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
366    pub(crate) fn retains_publication(&self, scope: &crate::plugin::PluginScope) -> bool {
367        self.source.check().is_ok_and(|origin| {
368            (origin.has_lifecycle_loan() || origin.resident) && origin.scope.as_ref().is_some_and(|caller| caller.same_activation(scope))
369        })
370    }
371
372    pub fn submit_at<O: OperationDeclaration>(&self, id: &str, request: O::Request) -> Result<Running<O>> {
373        self.operations.admit_at::<O, ()>(id, (request.into(), ()), Some(&self.scope), self.admission.held(), &self.source, None)
374    }
375
376    pub fn submit<O: OperationDeclaration>(&self, request: O::Request) -> Result<Running<O>> {
377        self.operations.submit_scoped::<O, ()>((request.into(), ()), &self.scope, &self.admission, &self.source, None)
378    }
379
380    pub fn request_claims(&self) -> Option<&super::RequestClaims> {
381        self.request_claims.as_ref()
382    }
383
384    pub fn submit_owned<O: OperationDeclaration>(&self, request: O::Request, claims: super::RequestClaims) -> Result<Running<O>> {
385        self.operations.submit_scoped::<O, ()>(
386            (super::binding::Request::owned(request, claims), ()),
387            &self.scope,
388            &self.admission,
389            &self.source,
390            None,
391        )
392    }
393
394    pub fn submit_endpoints<O: OperationDeclaration, P: Send + 'static>(&self, request: O::Request, endpoints: P) -> Result<Running<O>> {
395        self.operations.submit_scoped::<O, P>((request.into(), endpoints), &self.scope, &self.admission, &self.source, None)
396    }
397
398    pub fn submit_charged<O: OperationDeclaration, P: Send + 'static>(
399        &self, request: super::StreamItem<O::Request>, endpoints: P,
400    ) -> Result<Running<O>> {
401        self.operations.submit_scoped::<O, P>(
402            (super::binding::Request::charged(request)?, endpoints),
403            &self.scope,
404            &self.admission,
405            &self.source,
406            None,
407        )
408    }
409
410    pub fn submit_caused<O: OperationDeclaration>(&self, request: O::Request, cause: Position) -> Result<Running<O>> {
411        self.operations.submit_scoped::<O, ()>((request.into(), ()), &self.scope, &self.admission, &self.source, Some(cause))
412    }
413
414    pub async fn call_caused<O: OperationDeclaration>(
415        &self, request: O::Request, cause: Position,
416    ) -> Result<crate::operations::Response<O::Response>> {
417        self.submit_caused::<O>(request, cause)?.await
418    }
419
420    pub async fn call<O: OperationDeclaration>(&self, request: O::Request) -> Result<crate::operations::Response<O::Response>> {
421        self.submit::<O>(request)?.await
422    }
423}
424
425/* === Public Functions === */
426
427impl HostContext {
428    pub fn request_claims(&self) -> Option<&super::RequestClaims> {
429        self.request_claims.as_ref()
430    }
431    pub fn borrow(&self) -> Context<'_> {
432        Context {
433            request_claims: self.request_claims.clone(),
434            scope: Scope::over(&self.scope),
435            id: self.id,
436            attempt: self.attempt,
437            generation: self.generation,
438            operations: self.operations.clone(),
439            admission: self.admission.clone(),
440            source: self.source.clone(),
441            origin: self.origin.clone(),
442            #[cfg(feature = "plugin-sidecar")]
443            retirement: self.retirement,
444        }
445    }
446}
447
448impl harmos_sdk::operations::Host for HostContext {
449    fn publish_on<S: harmos_domain::StreamSpec>(
450        &self, row: &S::Row,
451    ) -> impl std::future::Future<Output = std::result::Result<(), Self::Error>> + Send
452    where
453        S::Row: harmos_domain::Message,
454    {
455        use harmos_domain::Message;
456        let published = self.call::<harmos_operations::streams::Publish>(harmos_operations::streams::Publish {
457            stream: S::ID.to_owned(),
458            schema: S::VERSION,
459            sequence: None,
460            published_unix_micros: None,
461            context: None,
462            payload: row.encode(),
463            following: Vec::new(),
464        });
465        async move {
466            published.await?;
467            Ok(())
468        }
469    }
470
471    type Error = Error;
472    type Instance<O: OperationDeclaration> = Running<O>;
473
474    fn context(&self) -> &super::CallContext {
475        self.operations.call_context()
476    }
477
478    fn with_value<T: super::ContextValue>(&self, value: T) -> std::result::Result<Self, super::ContextError> {
479        let context = self.operations.call_context().with_value(value)?;
480        let mut child = self.clone();
481        child.operations = child.operations.with_context(context);
482        Ok(child)
483    }
484
485    async fn submit<O: OperationDeclaration>(&self, request: O::Request) -> Result<Running<O>> {
486        self.borrow().submit::<O>(request)
487    }
488
489    async fn submit_owned<O: OperationDeclaration>(&self, request: O::Request, claims: super::RequestClaims) -> Result<Running<O>> {
490        self.borrow().submit_owned::<O>(request, claims)
491    }
492
493    async fn call<O: OperationDeclaration>(&self, request: O::Request) -> Result<super::Response<O::Response>> {
494        self.borrow().submit::<O>(request)?.await
495    }
496}
497
498pub(crate) fn fresh_instance() -> Result<OperationInstanceId> {
499    NEXT_INSTANCE.try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)).map(OperationInstanceId::new).map_err(|_| {
500        Error::ResourceExhausted {
501            resource: "operation instance identities",
502        }
503    })
504}
505
506/* === Private Functions === */
507
508/* === Tests === */
509
510impl harmos_sdk::operations::instances::Instances for HostContext {
511    fn owned(&self, id: OperationInstanceId) -> Option<Arc<dyn harmos_sdk::operations::instances::InstanceControl>> {
512        self.borrow().owned_instance(id).map(|instance| instance as Arc<dyn harmos_sdk::operations::instances::InstanceControl>)
513    }
514}
515impl harmos_sdk::operations::instances::InstanceControl for crate::supervision::instance::Instance {
516    fn status(&self) -> harmos_domain::operations::instance::Status {
517        self.status().portable()
518    }
519    fn cancel(&self) {
520        self.cancel();
521    }
522    fn subscribe(&self) -> Box<dyn harmos_sdk::operations::instances::InstanceChanges> {
523        Box::new(InstanceChanges(self.settlement()))
524    }
525}
526struct InstanceChanges(tokio::sync::watch::Receiver<super::OperationStatus>);
527impl harmos_sdk::operations::instances::InstanceChanges for InstanceChanges {
528    fn current(&mut self) -> harmos_domain::operations::instance::Status {
529        self.0.borrow_and_update().portable()
530    }
531    fn changed(&mut self) -> harmos_sdk::operations::instances::Changed<'_> {
532        Box::pin(async { self.0.changed().await.is_ok() })
533    }
534}
535
536impl harmos_sdk::operations::access::Discovery for HostContext {
537    fn discover(
538        &self, after: Option<&str>, rows: usize, bytes: usize,
539    ) -> std::result::Result<harmos_sdk::operations::access::DiscoveryPage, harmos_sdk::operations::access::AccessError> {
540        let context = self.borrow();
541        let request = harmos_operations::host::Catalog {
542            kind: harmos_operations::host::CatalogKind::Operations,
543            schema: None,
544            after: after.map(str::to_owned),
545            rows: Some(rows),
546            bytes: Some(bytes),
547        };
548        let page = context.operations.discover(&context.source, &request)?;
549        Ok((page.entries, page.next))
550    }
551}
552impl harmos_sdk::operations::access::GrantAuthority for HostContext {
553    fn admit(&self, grant: &harmos_domain::plugin::Grant) -> std::result::Result<(), harmos_sdk::operations::access::AccessError> {
554        self.borrow().admit_stream_grant(grant).map_err(Into::into)
555    }
556}
557
558#[cfg(feature = "plugin-sidecar")]
559impl harmos_sdk::operations::publication::PublicationHost for HostContext {
560    fn publish<'a>(
561        &'a self, _: harmos_sdk::operations::CallContext, publication: harmos_sdk::operations::publication::Publication<'a>,
562    ) -> harmos_sdk::operations::publication::Published<'a> {
563        Box::pin(async move {
564            let context = self.borrow();
565            let scope = context.caller_scope().ok_or(Error::<std::convert::Infallible>::InvalidInput {
566                field: "native publication",
567                reason: "the original plugin caller is required",
568            })?;
569            let endpoint = scope.native().ok_or(Error::<std::convert::Infallible>::NotReady { domain: "native publication endpoint" })?;
570            endpoint.publish(scope, publication, &context).await.map_err(Into::into)
571        })
572    }
573}