1use std::future::Future;
21
22use super::caller::Source;
26use super::{CallContext, Context, ContextError, ContextValue, Manager, OperationDeclaration, Response, Running};
27use super::{EndpointSet, Lane, RetryPolicy};
28use crate::{Error, Result};
29
30#[must_use = "an invocation runs only when start or run is awaited"]
39pub struct Invocation<O: OperationDeclaration, L: Lane, E: EndpointSet = ()> {
40 target: Target,
41 request: O::Request,
42 retry: Option<RetryPolicy>,
43 lane: Option<L>,
44 endpoints: E,
45}
46
47enum Target {
50 Host(Manager),
51 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
52 Plugin(crate::plugin::PluginScope),
53}
54
55impl<O: OperationDeclaration, L: Lane> Invocation<O, L> {
60 pub(crate) fn new(operations: Manager, request: O::Request) -> Self {
61 Self {
62 target: Target::Host(operations),
63 request,
64 retry: None,
65 lane: None,
66 endpoints: (),
67 }
68 }
69}
70
71impl<O: OperationDeclaration, L: Lane, E: EndpointSet> Invocation<O, L, E> {
72 pub fn retry(mut self, policy: RetryPolicy) -> Self {
73 self.retry = Some(policy);
74 self
75 }
76
77 pub fn lane(mut self, lane: L) -> Self {
78 self.lane = Some(lane);
79 self
80 }
81
82 pub fn endpoints<F: EndpointSet>(self, endpoints: F) -> Invocation<O, L, F> {
83 Invocation {
84 target: self.target,
85 request: self.request,
86 retry: self.retry,
87 lane: self.lane,
88 endpoints,
89 }
90 }
91
92 pub async fn start(self) -> Result<Running<O>> {
93 if E::item_types() != super::item_types::<O>() {
94 return Err(Error::InvalidInput {
95 field: "operation endpoints",
96 reason: "endpoint items differ from the declaration",
97 });
98 }
99 let mut request = super::binding::Request::from(self.request);
100 request.retry = self.retry;
101 request.lane = self.lane.map(|lane| lane.name());
102 let (operations, source) = match self.target {
103 Target::Host(operations) => (operations, Source::Host),
104 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
105 Target::Plugin(scope) => (scope.operation_manager()?, Source::Scope(scope)),
106 };
107 operations.admit::<O, E>((request, self.endpoints), None, &[], &source, None)
108 }
109
110 pub async fn run(self) -> Result<Response<O::Response>> {
111 self.start().await?.join().await
112 }
113}
114
115#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
116impl crate::plugin::PluginScope {
117 pub fn operation<O: OperationDeclaration<Request = O>, L: Lane>(&self, request: O) -> Invocation<O, L> {
118 Invocation {
119 target: Target::Plugin(self.clone()),
120 request,
121 retry: None,
122 lane: None,
123 endpoints: (),
124 }
125 }
126
127 pub async fn register<L: Lane>(&self, mut registration: super::Registration<L>) -> Result<()> {
128 if !self.is_ready() {
129 return Err(Error::Closing { domain: "plugin scope" });
130 }
131 let operations = self.operation_manager()?;
132 let services = std::mem::take(&mut registration.batch.services);
133 if services.is_empty() {
134 let entries = operations.native_entries(registration, self.owner(), Some(self))?;
135 return operations.reserve(&entries);
136 }
137 let mut batch = harmos_sdk::RegistrationBatch::new(registration.batch.dependencies.clone());
138 batch.services = services;
139 let manager = crate::ServiceManager::scoped(operations.clone(), self.clone());
140 let (staged, ids) = manager.stage(batch)?;
141 operations.register_scoped_services(registration, staged, self.owner(), Some(self))?;
142 manager.activate(&ids).await
143 }
144
145 pub async fn unregister(&self, id: &harmos_domain::QualifiedId) -> Result<()> {
146 let (owner, _) = id.as_str().split_once('.').expect("validated qualified identity");
147 if owner != self.owner() {
148 return Err(Error::PermissionDenied);
149 }
150 let operations = self.operation_manager()?;
151 if matches!(operations.resident.definition(id.as_str()), Some(super::resident::Definition::Service(_))) {
152 return crate::ServiceManager::scoped(operations, self.clone()).unregister(id).await;
153 }
154 operations.unregister(id).await
155 }
156}
157
158impl<A: crate::Application> crate::Runtime<A> {
159 pub fn services(&self) -> crate::ServiceManager {
160 crate::ServiceManager::new(self.operations.execution())
161 }
162
163 pub async fn register(&self, mut registration: super::Registration<A::Lanes>) -> Result<()> {
164 let services = std::mem::take(&mut registration.batch.services);
165 if services.is_empty() {
166 return self.operations.register(registration).await;
167 }
168 let mut batch = harmos_sdk::RegistrationBatch::new(registration.batch.dependencies.clone());
169 batch.services = services;
170 let manager = self.services();
171 let (staged, ids) = manager.stage(batch)?;
172 self.operations.execution().register_with_services(registration, staged)?;
173 manager.activate(&ids).await
174 }
175
176 pub async fn unregister(&self, id: &harmos_domain::QualifiedId) -> Result<()> {
177 if !id.as_str().starts_with("host.") {
178 return Err(Error::PermissionDenied);
179 }
180 if matches!(self.operations.resident.definition(id.as_str()), Some(super::resident::Definition::Service(_))) {
181 return self.services().unregister(id).await;
182 }
183 self.operations.unregister(id).await
184 }
185
186 pub fn operation<O: OperationDeclaration<Request = O>>(&self, request: O) -> Invocation<O, A::Lanes> {
187 Invocation::new(self.operations.execution(), request)
188 }
189}
190
191impl<A: crate::Application> crate::runtime::Handle<A> {
192 pub fn services(&self) -> crate::ServiceManager {
193 crate::ServiceManager::new(self.operations.execution())
194 }
195
196 pub async fn register(&self, mut registration: super::Registration<A::Lanes>) -> Result<()> {
197 let services = std::mem::take(&mut registration.batch.services);
198 if services.is_empty() {
199 return self.operations.register(registration).await;
200 }
201 let mut batch = harmos_sdk::RegistrationBatch::new(registration.batch.dependencies.clone());
202 batch.services = services;
203 let manager = self.services();
204 let (staged, ids) = manager.stage(batch)?;
205 self.operations.execution().register_with_services(registration, staged)?;
206 manager.activate(&ids).await
207 }
208
209 pub async fn unregister(&self, id: &harmos_domain::QualifiedId) -> Result<()> {
210 if !id.as_str().starts_with("host.") {
211 return Err(Error::PermissionDenied);
212 }
213 if matches!(self.operations.resident.definition(id.as_str()), Some(super::resident::Definition::Service(_))) {
214 return self.services().unregister(id).await;
215 }
216 self.operations.unregister(id).await
217 }
218
219 pub fn operation<O: OperationDeclaration<Request = O>>(&self, request: O) -> Invocation<O, A::Lanes> {
220 Invocation::new(self.operations.execution(), request)
221 }
222}
223
224impl<L: Lane> harmos_sdk::operations::Host for super::OperationManager<L> {
225 fn publish_on<S: harmos_domain::StreamSpec>(
226 &self, row: &S::Row,
227 ) -> impl std::future::Future<Output = std::result::Result<(), Self::Error>> + Send
228 where
229 S::Row: harmos_domain::Message,
230 {
231 use harmos_domain::Message;
232 let published = self.call::<harmos_operations::streams::Publish>(harmos_operations::streams::Publish {
233 stream: S::ID.to_owned(),
234 schema: S::VERSION,
235 sequence: None,
236 published_unix_micros: None,
237 context: None,
238 payload: row.encode(),
239 following: Vec::new(),
240 });
241 async move {
242 published.await?;
243 Ok(())
244 }
245 }
246
247 type Error = Error;
248 type Instance<O: OperationDeclaration> = Running<O>;
249
250 fn context(&self) -> &CallContext {
251 self.call_context()
252 }
253
254 fn with_value<T: ContextValue>(&self, value: T) -> std::result::Result<Self, ContextError> {
255 let context = self.call_context().with_value(value)?;
256 Ok(Self::new(self.execution().with_context(context)))
257 }
258
259 fn submit<O: OperationDeclaration>(&self, request: O::Request) -> impl Future<Output = Result<Running<O>>> + Send {
260 std::future::ready(Manager::submit::<O>(self, request))
261 }
262
263 fn submit_owned<O: OperationDeclaration>(
264 &self, request: O::Request, claims: super::RequestClaims,
265 ) -> impl Future<Output = Result<Running<O>>> + Send {
266 std::future::ready(Manager::submit_owned::<O>(self, request, claims))
267 }
268
269 fn call<O: OperationDeclaration>(&self, request: O::Request) -> impl Future<Output = Result<Response<O::Response>>> + Send {
270 let submitted = Manager::submit::<O>(self, request);
271 async move { submitted?.await }
272 }
273}
274
275impl harmos_sdk::operations::Host for Manager {
276 fn publish_on<S: harmos_domain::StreamSpec>(
277 &self, row: &S::Row,
278 ) -> impl std::future::Future<Output = std::result::Result<(), Self::Error>> + Send
279 where
280 S::Row: harmos_domain::Message,
281 {
282 use harmos_domain::Message;
283 let published = self.call::<harmos_operations::streams::Publish>(harmos_operations::streams::Publish {
284 stream: S::ID.to_owned(),
285 schema: S::VERSION,
286 sequence: None,
287 published_unix_micros: None,
288 context: None,
289 payload: row.encode(),
290 following: Vec::new(),
291 });
292 async move {
293 published.await?;
294 Ok(())
295 }
296 }
297
298 type Error = Error;
299 type Instance<O: OperationDeclaration> = Running<O>;
300
301 fn context(&self) -> &CallContext {
302 self.call_context()
303 }
304
305 fn with_value<T: ContextValue>(&self, value: T) -> std::result::Result<Self, ContextError> {
306 let context = self.call_context().with_value(value)?;
307 Ok(self.with_context(context))
308 }
309
310 fn submit<O: OperationDeclaration>(&self, request: O::Request) -> impl Future<Output = Result<Running<O>>> + Send {
311 std::future::ready(Manager::submit::<O>(self, request))
312 }
313
314 fn submit_owned<O: OperationDeclaration>(
315 &self, request: O::Request, claims: super::RequestClaims,
316 ) -> impl Future<Output = Result<Running<O>>> + Send {
317 std::future::ready(Manager::submit_owned::<O>(self, request, claims))
318 }
319
320 fn call<O: OperationDeclaration>(&self, request: O::Request) -> impl Future<Output = Result<Response<O::Response>>> + Send {
321 let submitted = Manager::submit::<O>(self, request);
322 async move { submitted?.await }
323 }
324}
325
326impl harmos_sdk::operations::Host for Context<'_> {
327 fn publish_on<S: harmos_domain::StreamSpec>(
328 &self, row: &S::Row,
329 ) -> impl std::future::Future<Output = std::result::Result<(), Self::Error>> + Send
330 where
331 S::Row: harmos_domain::Message,
332 {
333 use harmos_domain::Message;
334 let published = self.call::<harmos_operations::streams::Publish>(harmos_operations::streams::Publish {
335 stream: S::ID.to_owned(),
336 schema: S::VERSION,
337 sequence: None,
338 published_unix_micros: None,
339 context: None,
340 payload: row.encode(),
341 following: Vec::new(),
342 });
343 async move {
344 published.await?;
345 Ok(())
346 }
347 }
348
349 type Error = Error;
350 type Instance<O: OperationDeclaration> = Running<O>;
351
352 fn context(&self) -> &CallContext {
353 Context::context(self)
354 }
355
356 fn with_value<T: ContextValue>(&self, value: T) -> std::result::Result<Self, ContextError> {
357 let context = Context::context(self).with_value(value)?;
358 let mut child = self.clone();
359 child.operations = child.operations.with_context(context);
360 Ok(child)
361 }
362
363 fn submit<O: OperationDeclaration>(&self, request: O::Request) -> impl Future<Output = Result<Running<O>>> + Send {
364 std::future::ready(Context::submit::<O>(self, request))
365 }
366
367 fn submit_owned<O: OperationDeclaration>(
368 &self, request: O::Request, claims: super::RequestClaims,
369 ) -> impl Future<Output = Result<Running<O>>> + Send {
370 std::future::ready(Context::submit_owned::<O>(self, request, claims))
371 }
372
373 fn call<O: OperationDeclaration>(&self, request: O::Request) -> impl Future<Output = Result<Response<O::Response>>> + Send {
374 let submitted = Context::submit::<O>(self, request);
375 async move { submitted?.await }
376 }
377
378 }
381
382#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::{Application, RegistrationExt};
392 use std::sync::{
393 Arc,
394 atomic::{AtomicUsize, Ordering},
395 };
396
397 #[crate::lanes]
398 enum Lanes {
399 #[default]
400 Default,
401 }
402
403 struct Model;
404 impl Application for Model {
405 type Lanes = Lanes;
406 type State = ();
407 type Transaction = crate::state::Never;
408 type Record = ();
409 type Streams = ();
410 type Resources = ();
411 }
412
413 #[crate::operation(harmos_path = crate, resources = [AtomicUsize, crate::operations::HostContext],
414 id = "test.probe", response = u64,
415 policy = crate::OperationPolicy {
416 timeout: std::time::Duration::from_secs(1),
417 attempt_timeout: std::time::Duration::from_secs(1),
418 retries: RetryPolicy::Never,
419 },
420 )]
421 struct Probe {
422 value: u64,
423 }
424
425 impl crate::Operation for Probe {
426 type Error = std::io::Error;
427
428 async fn run(self, ctx: crate::Context<'_, Self>) -> std::io::Result<Response<u64>> {
429 let host = ctx.resource::<crate::operations::HostContext>();
430 let attempt = host.borrow().attempt();
431 if attempt == 0 {
432 ctx.resource::<AtomicUsize>().fetch_add(1, Ordering::SeqCst);
433 }
434 if u64::try_from(attempt).unwrap() < self.value {
435 return Err(std::io::Error::other("retry this request"));
436 }
437 Ok(0.into())
438 }
439
440 fn classify(_: &std::io::Error) -> super::super::FailureKind {
441 super::super::FailureKind::Transient
442 }
443 }
444
445 struct Idle;
446
447 impl harmos_sdk::Service for Idle {
448 type Error = std::io::Error;
449 async fn initialize(_: harmos_sdk::ExecutionContext) -> std::io::Result<Self> {
450 Ok(Self)
451 }
452 async fn run(&mut self, _: harmos_sdk::ExecutionContext) -> std::io::Result<()> {
453 std::future::pending().await
454 }
455 }
456
457 #[tokio::test]
458 async fn mixed_registration_releases_invalid_batch_reservations() {
459 let runtime = crate::Runtime::<Model>::builder((), ()).register(Lanes::catalog()).runtime().await.unwrap();
460 let mut dependencies = harmos_sdk::Dependencies::new();
461 dependencies.insert(Arc::new(AtomicUsize::new(0))).unwrap();
462 let batch = harmos_sdk::RegistrationBatch::new(dependencies).operation::<Probe>().service::<Idle>(
463 "idle",
464 crate::ServicePolicy {
465 stop_timeout: std::time::Duration::from_secs(1),
466 restart: crate::RestartPolicy::Never,
467 },
468 );
469 assert!(runtime.register(batch.clone().operation::<Probe>().defaults()).await.is_err());
470 assert!(runtime.operations.definition(Probe::ID).is_err());
471 assert!(runtime.operations.resident.definition("host.idle").is_none());
472 runtime.register(batch.defaults()).await.unwrap();
473 assert!(runtime.operations.definition(Probe::ID).is_ok());
474 assert!(runtime.operations.resident.definition("host.idle").is_some());
475 runtime.unregister(&crate::QualifiedId::parse("host.idle").unwrap()).await.unwrap();
476 runtime.unregister(&crate::QualifiedId::parse("host.test.probe").unwrap()).await.unwrap();
477 runtime.stop().await.unwrap();
478 }
479
480 #[tokio::test]
481 async fn operation_and_service_share_one_identity_namespace() {
482 let runtime = crate::Runtime::<Model>::builder((), ()).register(Lanes::catalog()).runtime().await.unwrap();
483 let mut dependencies = harmos_sdk::Dependencies::new();
484 dependencies.insert(Arc::new(AtomicUsize::new(0))).unwrap();
485 let operation = harmos_sdk::RegistrationBatch::new(dependencies).operation::<Probe>();
486 let policy = crate::ServicePolicy {
487 stop_timeout: std::time::Duration::from_secs(1),
488 restart: crate::RestartPolicy::Never,
489 };
490 let service = harmos_sdk::RegistrationBatch::new(harmos_sdk::Dependencies::new()).service::<Idle>("test.probe", policy);
491 assert!(runtime.register(operation.clone().service::<Idle>("test.probe", policy).defaults()).await.is_err());
492 assert!(runtime.operations.definition(Probe::ID).is_err());
493 assert!(!runtime.operations.resident.contains("host.test.probe"));
494 runtime.operations.register(operation.clone().defaults()).await.unwrap();
495 assert!(runtime.services().register(service.clone()).await.is_err());
496 runtime.unregister(&crate::QualifiedId::parse("host.test.probe").unwrap()).await.unwrap();
497 runtime.services().register(service).await.unwrap();
498 assert!(runtime.operations.register(operation.defaults()).await.is_err());
499 runtime.unregister(&crate::QualifiedId::parse("host.test.probe").unwrap()).await.unwrap();
500 runtime.stop().await.unwrap();
501 }
502
503 #[tokio::test]
504 async fn native_registration_is_atomic_and_invocation_policy_is_local() {
505 let runtime = crate::Runtime::<Model>::builder((), ()).register(Lanes::catalog()).runtime().await.unwrap();
506 let initialized = Arc::new(AtomicUsize::new(0));
507 let mut dependencies = harmos_sdk::Dependencies::new();
508 dependencies.insert(initialized.clone()).unwrap();
509 let batch = harmos_sdk::RegistrationBatch::new(dependencies).operation::<Probe>();
510 assert!(runtime.register(batch.clone().operation::<Probe>().defaults()).await.is_err());
511 assert!(runtime.operations.definition(Probe::ID).is_err());
512 let mut mismatched = batch.clone();
513 mismatched.operations[0].definition.id = crate::Identifier::new("different").unwrap();
514 assert!(runtime.register(mismatched.defaults()).await.is_err());
515 assert!(runtime.operations.definition("different").is_err());
516 runtime.operations.register(batch.defaults()).await.unwrap();
517 assert!(matches!(
518 runtime.operations.call_encoded("host.test.probe", Vec::new()).await,
519 Err(Error::InvalidInput { field: "operation preparation bytes", .. })
520 ));
521 let invocation = runtime.operation(Probe { value: 1 });
522 assert_eq!(initialized.load(Ordering::SeqCst), 0);
523 assert!(invocation.run().await.is_err());
524 assert_eq!(initialized.load(Ordering::SeqCst), 1);
525
526 let retry = RetryPolicy::OnTransient {
527 max_retries: 1,
528 backoff: crate::Backoff::Fixed {
529 delay: std::time::Duration::from_millis(1),
530 jitter: crate::Jitter::None,
531 },
532 };
533 assert_eq!(*runtime.operation(Probe { value: 1 }).retry(retry).run().await.unwrap(), 0);
534 assert_eq!(initialized.load(Ordering::SeqCst), 2);
535 assert!(runtime.operation(Probe { value: 1 }).run().await.is_err());
536 assert_eq!(initialized.load(Ordering::SeqCst), 3);
537 assert_eq!(runtime.operations.definition(Probe::ID).unwrap().policy.retries, RetryPolicy::Never);
538 runtime.operations.unregister(&crate::QualifiedId::parse("host.test.probe").unwrap()).await.unwrap();
539 assert!(runtime.operation(Probe { value: 0 }).start().await.is_err());
540 runtime.stop().await.unwrap();
541 }
542}