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 #[cfg(feature = "plugin-sidecar")]
102 retirement: bool,
103}
104
105impl 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 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 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 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 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 #[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 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 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 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 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 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 #[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
424impl 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
505impl 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}