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