1use std::future::Future;
18use std::panic::{AssertUnwindSafe, catch_unwind};
19use std::pin::Pin;
20use std::sync::{Arc, Weak};
21use std::task::{Context, Poll};
22use std::time::Duration;
23use tokio::sync::OwnedSemaphorePermit;
25use super::caller::Source;
27use super::manager::{Entry, Submission};
28use super::{Manager, OperationInstanceId};
29use crate::operations::OperationDeclaration;
30use crate::supervision::executor;
31use crate::{Error, Refusal, Result, operations::Scope};
32
33type Outcome = Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send>>;
40pub(super) type Dispatch = fn(&Manager, Submission, Source, Option<Parent<'_>>, EncodedInput) -> Result<EncodedInvocation>;
41
42struct Encoded;
45
46pub(super) struct EncodedInput {
47 pub(super) payload: Vec<u8>,
48 pub(super) preparation: Option<OwnedSemaphorePermit>,
49
50 #[cfg(feature = "plugin-sidecar")]
51 pub(super) native_capacity: Option<OwnedSemaphorePermit>,
52}
53
54#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
55#[derive(Clone)]
56pub struct PluginCallback {
57 operations: Manager,
58 entry: Arc<Entry>,
59 contract: crate::operations::Contract,
60}
61
62#[cfg(feature = "plugin-sidecar")]
63#[derive(Clone)]
64pub(crate) struct PluginControl(PluginCallback);
65
66pub(super) struct Parent<'a> {
67 pub(super) scope: &'a Scope<'a>,
68 pub(super) ancestors: &'a [&'static str],
69}
70
71pub struct EncodedInvocation {
72 pub(super) id: OperationInstanceId,
73 pub(super) generation: super::OperationGeneration,
74 pub(super) outcome: Outcome,
75 pub(crate) control: Arc<crate::supervision::instance::Instance>,
76}
77
78impl OperationDeclaration for Encoded {
85 type Request = Vec<u8>;
86 type Response = Vec<u8>;
87 type Input = super::NoStream;
88 type Output = super::NoStream;
89 const ID: &'static str = "encoded";
90 const POLICY: super::OperationPolicy = super::OperationPolicy {
91 timeout: harmos_domain::operations::UNBOUNDED,
92 attempt_timeout: harmos_domain::operations::UNBOUNDED,
93 retries: super::RetryPolicy::Never,
94 };
95}
96
97impl EncodedInvocation {
98 pub fn id(&self) -> OperationInstanceId {
99 self.id
100 }
101
102 pub fn generation(&self) -> super::OperationGeneration {
103 self.generation
104 }
105
106 pub fn cancel(&self) {
107 self.control.cancel();
108 }
109
110 pub fn status(&self) -> super::OperationStatus {
111 self.control.status()
112 }
113
114 #[tracing::instrument(target = "harmos::operations", name = "operations.join", level = "debug", skip_all)]
115 pub async fn join(self) -> Result<Vec<u8>> {
116 self.await
117 }
118
119 #[tracing::instrument(target = "harmos::operations", name = "operations.settle", level = "debug", skip_all)]
120 pub async fn settle(&self) -> Result<()> {
121 self.control.settle().await.map_err(|source| Error::OperationSettlement { source })
122 }
123}
124
125impl Future for EncodedInvocation {
126 type Output = Result<Vec<u8>>;
127
128 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
129 self.get_mut().outcome.as_mut().poll(context)
130 }
131}
132
133impl Manager {
134 pub fn submit_encoded(&self, operation: &str, payload: Vec<u8>) -> Result<EncodedInvocation> {
135 self.submit_encoded_as(operation, payload, Source::Host, None)
136 }
137
138 pub async fn call_encoded(&self, operation: &str, payload: Vec<u8>) -> Result<Vec<u8>> {
139 self.submit_encoded(operation, payload)?.await
140 }
141
142 pub(super) fn submit_encoded_as(
143 &self, operation: &str, payload: Vec<u8>, source: Source, parent: Option<Parent<'_>>,
144 ) -> Result<EncodedInvocation> {
145 self.submit_encoded_at_as(operation, payload, source, parent, executor::now(), None)
146 }
147
148 #[tracing::instrument(target = "harmos::operations", name = "operations.submit_encoded_at_as", level = "debug", skip_all)]
149 fn submit_encoded_at_as(
150 &self, operation: &str, payload: Vec<u8>, source: Source, parent: Option<Parent<'_>>, submitted: tokio::time::Instant,
151 expected: Option<([u8; 32], u64)>,
152 ) -> Result<EncodedInvocation> {
153 let origin = source.check()?;
154 let qualified = crate::QualifiedId::parse(operation).map_err(|_| Error::InvalidInput {
155 field: "encoded operation",
156 reason: "routing requires an owner-qualified operation identity",
157 })?;
158 let (owner, local) = qualified.as_str().split_once('.').expect("validated qualified identity");
159 let entry = self.capture_routed(local, owner, &origin).map_err(|error| match error {
160 Error::NotRegistered { .. } => Error::NotRegistered {
161 domain: "operation",
162 name: operation.to_owned(),
163 },
164 error => error,
165 })?;
166 entry.available_to(&origin)?;
167 entry.authorize(&origin)?;
168 if expected.is_some_and(|(fingerprint, generation)| {
169 entry.declared().fingerprint.0 != fingerprint || entry.generation.value() != generation
170 }) {
171 return Err(Error::InvalidInput {
172 field: "encoded operation identity",
173 reason: "fingerprint or generation differs from the assigned target",
174 });
175 }
176 let dispatch = entry.wire.ok_or(Error::InvalidInput {
177 field: "encoded operation",
178 reason: "operation has no cross-boundary codec binding",
179 })?;
180
181 dispatch(
182 self,
183 Submission {
184 submitted,
185 entry,
186 cause: None,
187 preparation: None,
188 },
189 source,
190 parent,
191 EncodedInput {
192 payload,
193 preparation: None,
194 #[cfg(feature = "plugin-sidecar")]
195 native_capacity: None,
196 },
197 )
198 }
199}
200
201#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
202impl Manager {
203 #[cfg(feature = "plugin-sidecar")]
204 pub(crate) fn capture_host_control(
205 &self, scope: &crate::plugin::PluginScope, contract: &crate::operations::Contract,
206 ) -> Result<PluginControl> {
207 if contract.operation().visibility != crate::operations::Visibility::Host || scope.observe_contract() != Some(contract) {
208 return Err(Error::InvalidInput {
209 field: "observer control",
210 reason: "control differs from the frozen Host contract",
211 });
212 }
213 self.capture_callback(scope, contract).map(PluginControl)
214 }
215
216 pub fn capture_callback(&self, scope: &crate::plugin::PluginScope, contract: &crate::operations::Contract) -> Result<PluginCallback> {
217 if scope.is_closed() {
218 return Err(Error::Closing { domain: "callback scope" });
219 }
220 let entry = self.capture_provider(contract.operation().id.as_str(), Some(scope.owner()))?;
221 let selected = entry.plugin().ok_or(Error::InvalidInput {
222 field: "callback scope",
223 reason: "callback target has no original plugin generation",
224 })?;
225 if !selected.same_activation(scope) {
226 return Err(Error::InvalidInput {
227 field: "callback scope",
228 reason: "callback target belongs to a different plugin generation",
229 });
230 }
231 PluginCallback::from_entry(self.clone(), entry, contract)
232 }
233}
234
235#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
236impl PluginCallback {
237 pub(super) fn from_entry(operations: Manager, entry: Arc<Entry>, contract: &crate::operations::Contract) -> Result<Self> {
238 if entry.declared() != contract.operation() || entry.contract.as_ref() != Some(contract) {
239 return Err(Error::InvalidInput {
240 field: "callback contract",
241 reason: "callback requires the exact retained canonical contract",
242 });
243 }
244 if entry.wire.is_none() {
245 return Err(Error::InvalidInput {
246 field: "callback codec",
247 reason: "callback target has no original codec binding",
248 });
249 }
250 Ok(Self {
251 operations,
252 entry,
253 contract: contract.clone(),
254 })
255 }
256
257 pub fn contract(&self) -> &crate::operations::Contract {
258 &self.contract
259 }
260
261 pub fn submit_with(
262 &self, encoding_memory_bytes: std::num::NonZeroUsize, encode: impl FnOnce(usize) -> Result<Vec<u8>>,
263 ) -> Result<EncodedInvocation> {
264 self.submit_with_as(Source::for_callback(&self.entry), encoding_memory_bytes, encode, false)
265 }
266
267 fn submit_with_as(
269 &self, source: Source, encoding_memory_bytes: std::num::NonZeroUsize, encode: impl FnOnce(usize) -> Result<Vec<u8>>, resident: bool,
270 ) -> Result<EncodedInvocation> {
271 #[cfg(not(feature = "plugin-sidecar"))]
272 let _ = resident;
273 let submitted = executor::now();
274 let origin = source.check()?;
275 self.entry.available()?;
276 self.entry.authorize(&origin)?;
277 let timeout = self.entry.declared().policy.timeout;
278 budget(timeout, submitted)?;
279 let preparation = self.operations.claim_preparation(encoding_memory_bytes)?;
280 #[cfg(feature = "plugin-sidecar")]
281 let native_capacity = self
282 .entry
283 .native
284 .as_ref()
285 .map(|binding| if resident { Ok(super::native::Binding::resident_slot()) } else { binding.reserve() })
286 .transpose()
287 .map_err(|source| Error::OperationFailed {
288 operation: self.entry.id().to_owned(),
289 source: Arc::new(source),
290 })?;
291 let payload = catch_unwind(AssertUnwindSafe(|| encode(encoding_memory_bytes.get())))
292 .map_err(|_| Error::OperationPanicked { operation: self.entry.id().to_owned() })??;
293 if payload.capacity() > encoding_memory_bytes.get() {
294 return Err(Error::ResourceExhausted { resource: "callback encoding claim" });
295 }
296 #[cfg(feature = "plugin-sidecar")]
297 if native_capacity.is_some() && payload.capacity() > harmos_sidecar::Frame::LIMIT {
298 return Err(Error::ResourceExhausted { resource: "native callback frame" });
299 }
300 source.check()?;
301 self.entry.available()?;
302 budget(timeout, submitted)?;
303 let dispatch = self.entry.wire.ok_or(Error::InvalidInput {
304 field: "callback codec",
305 reason: "callback target has no original codec binding",
306 })?;
307 dispatch(
308 &self.operations,
309 Submission {
310 submitted,
311 entry: Arc::clone(&self.entry),
312 cause: None,
313 preparation: None,
314 },
315 source,
316 None,
317 EncodedInput {
318 payload,
319 preparation: Some(preparation),
320 #[cfg(feature = "plugin-sidecar")]
321 native_capacity,
322 },
323 )
324 }
325}
326
327#[cfg(feature = "plugin-sidecar")]
328impl PluginControl {
329 pub(crate) fn submit_with(
330 &self, bytes: std::num::NonZeroUsize, encode: impl FnOnce(usize) -> Result<Vec<u8>>,
331 ) -> Result<EncodedInvocation> {
332 self.0.submit_with_as(Source::Host, bytes, encode, true)
333 }
334}
335
336#[cfg(any(feature = "plugin-sidecar", feature = "plugin-guest"))]
337impl super::Caller {
338 pub(crate) fn submit_encoded_at(
339 &self, operation: &str, fingerprint: [u8; 32], generation: u64, payload: Vec<u8>, submitted: tokio::time::Instant,
340 values: Vec<super::CallValue>,
341 ) -> Result<EncodedInvocation> {
342 let context = self.operations.call_context().with_values(values)?;
343 self.operations.with_context(context).submit_encoded_at_as(
344 operation,
345 payload,
346 Source::Plugin(self.source.clone()),
347 None,
348 submitted,
349 Some((fingerprint, generation)),
350 )
351 }
352}
353
354impl super::Context<'_> {
355 pub fn submit_encoded(&self, operation: &str, payload: Vec<u8>) -> Result<EncodedInvocation> {
356 self.operations.submit_encoded_as(
357 operation,
358 payload,
359 self.source.clone(),
360 Some(Parent {
361 scope: &self.scope,
362 ancestors: self.admission.held(),
363 }),
364 )
365 }
366
367 #[cfg(any(feature = "plugin-sidecar", all(feature = "plugin-guest", not(target_arch = "wasm32"))))]
368 pub(crate) fn submit_encoded_at(
369 &self, operation: &str, fingerprint: [u8; 32], generation: u64, payload: Vec<u8>, submitted: tokio::time::Instant,
370 values: Vec<super::CallValue>,
371 ) -> Result<EncodedInvocation> {
372 let context = self.operations.call_context().with_values(values)?;
373 self.operations.with_context(context).submit_encoded_at_as(
374 operation,
375 payload,
376 self.source.clone(),
377 Some(Parent {
378 scope: &self.scope,
379 ancestors: self.admission.held(),
380 }),
381 submitted,
382 Some((fingerprint, generation)),
383 )
384 }
385
386 pub async fn call_encoded(&self, operation: &str, payload: Vec<u8>) -> Result<Vec<u8>> {
387 self.submit_encoded(operation, payload)?.await
388 }
389}
390
391pub(super) fn dispatch_native(
394 operations: &Manager, mut submission: Submission, source: Source, parent: Option<Parent<'_>>, input: EncodedInput,
395) -> Result<EncodedInvocation> {
396 let submitted = submission.submitted;
397 let timeout = submission.entry.declared().policy.timeout;
398 budget(timeout, submitted)?;
399 let factory = submission.entry.binding.downcast_ref::<harmos_sdk::operations::OperationFactory>().ok_or(Error::InvalidInput {
400 field: "native operation",
401 reason: "entry does not own a native factory",
402 })?;
403 let name = submission.entry.id().to_owned();
404 submission.preparation = match input.preparation {
405 Some(original) => Some(original),
406 None => Some(operations.claim_preparation(std::num::NonZeroUsize::new(input.payload.capacity().max(1)).unwrap())?),
407 };
408 let decoding_bytes = catch_unwind(AssertUnwindSafe(|| factory.decoding_memory(&input.payload)))
409 .map_err(|_| Error::OperationPanicked { operation: name.clone() })?
410 .map_err(|source| Error::OperationPayload { operation: name.clone(), source })?;
411 if let Some(bytes) = std::num::NonZeroUsize::new(decoding_bytes) {
412 let decoded = operations.claim_preparation(bytes)?;
413 if let Some(original) = &mut submission.preparation {
414 original.merge(decoded);
415 } else {
416 submission.preparation = Some(decoded);
417 }
418 }
419 let prepared = catch_unwind(AssertUnwindSafe(|| factory.prepare_encoded(&input.payload)))
420 .map_err(|_| Error::OperationPanicked { operation: name.clone() })?
421 .map_err(|source| Error::OperationFailed {
422 operation: name,
423 source: Arc::new(source),
424 })?;
425 drop(input.payload);
426 dispatch_prepared(operations, submission, source, parent, prepared, super::binding::Request::from(Vec::new()))
427}
428
429pub(super) fn dispatch_prepared(
430 operations: &Manager, submission: Submission, source: Source, parent: Option<Parent<'_>>,
431 prepared: harmos_sdk::operations::registration::PreparedNative, mut request: super::binding::Request<Vec<u8>>,
432) -> Result<EncodedInvocation> {
433 let submitted = submission.submitted;
434 let timeout = submission.entry.declared().policy.timeout;
435 let harmos_sdk::operations::registration::PreparedNative { instance, grant, key, .. } = prepared;
436 let instance = std::sync::Mutex::new(Some(instance));
437 let binding = super::authored::factory_with::<Encoded, ()>(operations, move |_, ()| {
438 instance.lock().expect("native preparation lock is healthy").take().ok_or(harmos_sdk::ServingError::Uninitialized)
439 });
440 request.binding = Some(Arc::new(binding));
441 request.grant = grant;
442 request.key = key;
443 let target = Arc::downgrade(&submission.entry);
444 budget(timeout, submitted)?;
445 let (scope, ancestors) = parent.map_or((None, &[][..]), |parent| (Some(parent.scope), parent.ancestors));
446 let invocation = operations.admit_entry::<Encoded, ()>((request, ()), scope, ancestors, &source, submission)?;
447 let id = invocation.id();
448 let generation = invocation.generation();
449 let control = Arc::clone(&invocation.control);
450 let invocation = invocation.unguarded();
451 let outcome = Box::pin(async move {
452 let response = invocation.await?;
453 available(&target, &source)?;
454 budget(timeout, submitted)?;
455 Ok(response.try_unwrap().unwrap_or_else(|response| response.to_vec()))
456 });
457 Ok(EncodedInvocation { id, generation, outcome, control })
458}
459
460pub(super) fn available(target: &Weak<Entry>, source: &Source) -> Result<()> {
461 let origin = source.check()?;
462 target.upgrade().ok_or(Error::Closing { domain: "operation" })?.available_to(&origin)
463}
464
465pub(super) fn budget(timeout: Duration, submitted: tokio::time::Instant) -> Result<()> {
466 if executor::now().saturating_duration_since(submitted) >= timeout {
467 return Err(Refusal::TimedOut.into());
468 }
469 Ok(())
470}
471
472#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::RegistrationExt;
480
481 #[crate::lanes]
482 enum Lanes {
483 #[default]
484 Default,
485 }
486
487 struct Model;
488 impl crate::Application for Model {
489 type Lanes = Lanes;
490 type State = ();
491 type Transaction = crate::state::Never;
492 type Record = ();
493 type Streams = ();
494 type Resources = ();
495 }
496
497 #[crate::operation(harmos_path = crate, resources = [], id = "fenced.probe", response = u64,
498 policy = crate::OperationPolicy {
499 timeout: Duration::from_secs(1),
500 attempt_timeout: Duration::from_secs(1),
501 retries: crate::RetryPolicy::Never,
502 },
503 )]
504 struct Probe {
505 value: u64,
506 }
507
508 impl crate::Operation for Probe {
509 type Error = std::io::Error;
510
511 async fn run(self, _: crate::Context<'_, Self>) -> std::io::Result<crate::Response<u64>> {
512 Ok(self.value.into())
513 }
514 }
515
516 #[tokio::test]
517 async fn assigned_identity_fences_before_decoding() {
518 let builder = crate::Runtime::<Model>::builder((), ()).register(Lanes::catalog());
519 let runtime = builder.runtime().await.unwrap();
520 let batch = harmos_sdk::RegistrationBatch::new(harmos_sdk::Dependencies::new()).operation::<Probe>();
521 runtime.register(batch.clone().defaults()).await.unwrap();
522 let entry = runtime.operations.entry(Probe::ID).unwrap();
523 let fingerprint = entry.declared().fingerprint.0;
524 let generation = entry.generation.value();
525 let mut wrong = fingerprint;
526 wrong[0] ^= 1;
527
528 let submit = |fingerprint, generation| {
529 runtime.operations.submit_encoded_at_as(
530 "host.fenced.probe",
531 Vec::new(),
532 Source::Host,
533 None,
534 executor::now(),
535 Some((fingerprint, generation)),
536 )
537 };
538 assert!(matches!(submit(wrong, generation), Err(Error::InvalidInput { field: "encoded operation identity", .. })));
539 assert!(matches!(submit(fingerprint, generation), Err(Error::InvalidInput { field: "operation preparation bytes", .. })));
540
541 let id = crate::QualifiedId::parse("host.fenced.probe").unwrap();
542 runtime.unregister(&id).await.unwrap();
543 runtime.register(batch.defaults()).await.unwrap();
544 assert!(matches!(submit(fingerprint, generation), Err(Error::InvalidInput { field: "encoded operation identity", .. })));
545 runtime.stop().await.unwrap();
546 }
547}