1pub use harmos_domain::operations::instance::Id as OperationInstanceId;
15
16use std::future::Future;
20use std::pin::Pin;
21use std::sync::{
22 Arc,
23 atomic::{AtomicU64, Ordering},
24};
25use std::task::{Context as TaskContext, Poll};
26use tokio::sync::oneshot;
28
29use super::Manager;
31use super::admission::Admission;
32use super::caller::{Origin, Source};
33use crate::operations::OperationDeclaration;
34use crate::{Error, Result, operations::Scope, state::Position};
35
36static NEXT_INSTANCE: AtomicU64 = AtomicU64::new(1);
39static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub struct OperationGeneration(u64);
49
50#[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 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
106impl 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 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 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 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 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 #[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 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 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 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 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 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 #[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
425impl 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
506impl 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}