1mod attachment;
19
20pub use attachment::ObserverAttachment;
23pub use harmos_domain::inventory::observers::{Activation, DemandKey};
24
25use std::any::Any;
29use std::future::Future;
30use std::panic::AssertUnwindSafe;
31use std::pin::Pin;
32use std::sync::Arc;
33use std::time::Duration;
34use futures_util::FutureExt;
36use tokio::time::{Instant, MissedTickBehavior, interval};
37use super::identity::{ActivationId, Identity};
39use super::{ObserverCaller, ObserverHost, ObserverPublisher, ObserverStreamPublisher, PublicationError, Supervisor};
40use harmos_domain::inventory::publications;
41
42type ContextFactory<H> = Arc<dyn Fn(&H) -> Arc<dyn Any + Send + Sync> + Send + Sync>;
49type Open<H> = fn(Arc<harmos_sdk::ExecutionContext>, H, Option<ContextFactory<H>>, Identity) -> Result<Activity, ObserverError>;
50type Step<'a> = Pin<Box<dyn Future<Output = Result<(), ObserverError>> + Send + 'a>>;
51
52pub struct Declarations<H = Supervisor>(std::marker::PhantomData<H>);
55
56pub struct Inventory<H = Supervisor>(std::marker::PhantomData<H>);
57
58pub struct Outputs<H = Supervisor>(std::marker::PhantomData<H>);
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct Tick {
62 ordinal: u64,
63 elapsed: Duration,
64}
65
66pub struct ObserverContext<'a, H = Supervisor> {
67 identity: Identity,
68 resources: &'a harmos_sdk::ExecutionContext,
69 tick: &'a Tick,
70 host: &'a H,
71 streams: &'static [publications::Entry],
72 extension: Option<&'a (dyn Any + Send + Sync)>,
73}
74
75pub struct Instance<H = Supervisor> {
76 entry: harmos_domain::inventory::observers::Entry,
77 open: Open<H>,
78 context: Option<ContextFactory<H>>,
79}
80
81pub struct Activity {
85 identity: Identity,
86 body: Box<dyn Body>,
87}
88
89struct Bound<S: Observer<H>, H: Send + Sync + 'static> {
90 identity: Identity,
91 resources: Arc<harmos_sdk::ExecutionContext>,
92 host: H,
93 extension: Option<Arc<dyn Any + Send + Sync>>,
94 observer: Option<S>,
95 stopped: bool,
96}
97
98#[derive(Debug, thiserror::Error)]
101pub enum ObserverError {
102 #[error("the observer host or context factory panicked")]
103 BindingPanicked,
104
105 #[error("the observer activation identity could not be drawn: {0}")]
106 Identity(#[source] std::io::Error),
107
108 #[error("the observer callback panicked")]
109 CallbackPanicked,
110
111 #[error("the observer tick ordinal is exhausted")]
112 TickExhausted,
113
114 #[error("the observer refused to stop: {0}")]
115 StopFailed(#[source] Arc<dyn std::error::Error + Send + Sync>),
116
117 #[error("the observer panicked while stopping")]
118 StopPanicked,
119
120 #[error("the observer refused to clean up: {0}")]
121 CleanupFailed(#[source] Arc<dyn std::error::Error + Send + Sync>),
122
123 #[error("the observer panicked while cleaning up")]
124 CleanupPanicked,
125}
126
127trait Body: Send {
130 fn observe<'a>(&'a mut self, ready: &'a (dyn Fn() + Send + Sync)) -> Step<'a>;
131 fn stop(&mut self) -> Step<'_>;
132 fn cleanup(&mut self) -> Step<'_>;
133 fn is_initialized(&self) -> bool;
134}
135
136pub trait Observer<H: Send + Sync + 'static = Supervisor>: Sized + Send + 'static {
137 type Error: std::error::Error + Send + Sync + 'static;
138
139 const ID: &'static str;
140
141 const DESCRIPTION: &'static str = "";
142
143 const STREAMS: &'static [publications::Entry] = &[];
144
145 const DEMAND_KEYS: &'static [DemandKey] = &[];
146
147 const ACTIVATION: Activation = Activation::Plugin;
148
149 const INTERVAL: Duration;
150
151 fn initialize(resources: &harmos_sdk::ExecutionContext) -> impl Future<Output = Result<Self, Self::Error>> + Send;
152
153 fn observe(&mut self, ctx: &ObserverContext<'_, H>) -> impl Future<Output = Result<(), Self::Error>> + Send;
154
155 fn stop(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send {
156 std::future::ready(Ok(()))
157 }
158
159 fn cleanup(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send {
160 std::future::ready(Ok(()))
161 }
162}
163
164impl<H: Send + Sync + 'static> harmos_domain::catalog::Visitor for Declarations<H> {
167 type Entry = fn() -> Instance<H>;
168}
169
170impl<H: Send + Sync + 'static, S: Observer<H>> harmos_domain::catalog::Member<S> for Declarations<H> {
171 const ENTRY: fn() -> Instance<H> = Instance::of::<S>;
172}
173
174impl<H: Send + Sync + 'static> harmos_domain::catalog::Visitor for Outputs<H> {
175 type Entry = &'static [publications::Entry];
176}
177
178impl<H: Send + Sync + 'static, S: Observer<H>> harmos_domain::catalog::Member<S> for Outputs<H> {
179 const ENTRY: &'static [publications::Entry] = S::STREAMS;
180}
181
182impl<H: Send + Sync + 'static> harmos_domain::catalog::Visitor for Inventory<H> {
183 type Entry = harmos_domain::inventory::observers::Entry;
184}
185
186impl<H: Send + Sync + 'static, S: Observer<H>> harmos_domain::catalog::Member<S> for Inventory<H> {
187 const ENTRY: harmos_domain::inventory::observers::Entry = harmos_domain::inventory::observers::Entry {
188 id: S::ID,
189 description: S::DESCRIPTION,
190 interval: S::INTERVAL,
191 activation: S::ACTIVATION,
192 streams: S::STREAMS,
193 demand_keys: S::DEMAND_KEYS,
194 };
195}
196
197impl Tick {
198 fn advance(&mut self, elapsed: Duration) -> Result<(), ObserverError> {
199 let ordinal = self.ordinal.checked_add(1).ok_or(ObserverError::TickExhausted)?;
200 self.ordinal = ordinal;
201 self.elapsed = elapsed;
202 Ok(())
203 }
204
205 pub fn ordinal(&self) -> u64 {
206 self.ordinal
207 }
208
209 pub fn elapsed(&self) -> Duration {
210 self.elapsed
211 }
212}
213
214impl<H> ObserverContext<'_, H> {
215 pub fn observer_id(&self) -> &'static str {
216 self.identity.observer
217 }
218
219 pub fn activation_id(&self) -> ActivationId {
220 self.identity.activation
221 }
222
223 pub fn resource<T: Send + Sync + 'static>(&self) -> Result<Arc<T>, harmos_sdk::operations::ResourceError> {
224 self.resources.resource::<T>()
225 }
226
227 pub fn tick(&self) -> &Tick {
228 self.tick
229 }
230
231 pub fn host(&self) -> &H {
232 self.host
233 }
234
235 pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
236 let extension = self.extension?;
237 extension.downcast_ref()
238 }
239}
240
241impl ObserverContext<'_, Supervisor> {
242 pub fn supervisor(&self) -> &Supervisor {
243 self.host
244 }
245}
246
247impl<H> ObserverContext<'_, H> {
248 pub async fn publish<V: Send>(&self, value: V) -> Result<(), H::Error>
249 where
250 H: ObserverPublisher<V>,
251 {
252 if let Some(stream) = H::STREAM {
253 self.check_stream(stream.id, stream.schema)?;
254 }
255 self.host.publish(self, value).await
256 }
257
258 pub async fn publish_on<S: harmos_domain::StreamSpec>(&self, row: &S::Row) -> Result<(), H::Error>
259 where
260 S::Row: Sync,
261 H: ObserverStreamPublisher<S>,
262 {
263 self.check_stream(S::ID, S::VERSION)?;
264 self.host.publish_on(self, row).await
265 }
266
267 pub async fn call<C>(&self, request: H::Request) -> Result<H::Response, H::Error>
268 where
269 H: ObserverCaller<C>,
270 {
271 self.host.call(self, request).await
272 }
273
274 fn check_stream(&self, id: &'static str, version: u32) -> Result<(), PublicationError> {
275 if self.streams.iter().any(|entry| entry.id == id && entry.schema == version) {
276 Ok(())
277 } else {
278 Err(PublicationError::Undeclared { stream: id })
279 }
280 }
281}
282
283impl<H: Send + Sync + 'static> Instance<H> {
284 pub fn of<S: Observer<H>>() -> Self {
285 assert!(
286 !S::ID.trim().is_empty() && !S::ID.chars().any(char::is_control),
287 "an observer identity must be nonempty and contain no control characters"
288 );
289 assert!(!S::INTERVAL.is_zero(), "an observer interval must be nonzero");
290 assert!(
291 S::ACTIVATION != Activation::Demand || !S::STREAMS.is_empty() || !S::DEMAND_KEYS.is_empty(),
292 "a demand observer must declare streams or demand keys"
293 );
294
295 for key in S::DEMAND_KEYS {
296 assert!(!S::STREAMS.iter().any(|entry| entry.id == key.name()), "demand keys must differ from publication identities");
297 }
298
299 Self {
300 entry: <Inventory<H> as harmos_domain::catalog::Member<S>>::ENTRY,
301 open: Activity::open::<S, H>,
302 context: None,
303 }
304 }
305
306 pub fn with_context<T: Send + Sync + 'static>(mut self, factory: impl Fn(&H) -> T + Send + Sync + 'static) -> Self {
307 let erased = move |host: &H| -> Arc<dyn Any + Send + Sync> {
308 let extension = factory(host);
309 Arc::new(extension)
310 };
311 self.context = Some(Arc::new(erased));
312 self
313 }
314
315 pub fn declaration(&self) -> harmos_domain::inventory::observers::Declaration {
316 self.entry.declaration()
317 }
318
319 pub fn id(&self) -> &'static str {
320 self.entry.id
321 }
322
323 pub fn streams(&self) -> &'static [publications::Entry] {
324 self.entry.streams
325 }
326
327 pub fn demand_keys(&self) -> &'static [DemandKey] {
328 self.entry.demand_keys
329 }
330
331 pub fn activation(&self) -> Activation {
332 self.entry.activation
333 }
334
335 pub fn interval(&self) -> Duration {
336 self.entry.interval
337 }
338
339 pub fn activate(&self, resources: Arc<harmos_sdk::ExecutionContext>, host: H) -> Result<Activity, ObserverError> {
342 let identity = Identity::new(self.entry.id).map_err(ObserverError::Identity)?;
343 (self.open)(resources, host, self.context.clone(), identity)
344 }
345}
346
347impl Activity {
348 fn open<S: Observer<H>, H: Send + Sync + 'static>(
349 resources: Arc<harmos_sdk::ExecutionContext>, host: H, context: Option<ContextFactory<H>>, identity: Identity,
350 ) -> Result<Self, ObserverError> {
351 let create_extension = || match context {
352 Some(factory) => {
353 let extension = factory(&host);
354 Some(extension)
355 }
356 None => None,
357 };
358 let guarded_extension = AssertUnwindSafe(create_extension);
359 let extension = std::panic::catch_unwind(guarded_extension).map_err(|_| {
360 tracing::error!(
361 observer = identity.observer, activation_id = %identity.activation,
362 "observer context factory panicked; activation refused"
363 );
364 ObserverError::BindingPanicked
365 })?;
366 let bound = Bound::<S, H> {
367 identity,
368 resources,
369 host,
370 extension,
371 observer: None,
372 stopped: false,
373 };
374 Ok(Self { identity, body: Box::new(bound) })
375 }
376
377 pub fn observer_id(&self) -> &'static str {
378 self.identity.observer
379 }
380
381 pub fn activation_id(&self) -> ActivationId {
382 self.identity.activation
383 }
384
385 pub fn is_initialized(&self) -> bool {
386 self.body.is_initialized()
387 }
388
389 pub async fn observe(&mut self, ready: impl Fn() + Send + Sync) -> ObserverError {
393 match self.body.observe(&ready).await {
394 Ok(()) => unreachable!("observation runs until it is interrupted or fails"),
395 Err(failure) => failure,
396 }
397 }
398
399 pub async fn stop(&mut self) -> Result<(), ObserverError> {
400 self.body.stop().await
401 }
402
403 pub async fn cleanup(&mut self) -> Result<(), ObserverError> {
404 self.body.cleanup().await
405 }
406}
407
408impl<S: Observer<H>, H: Send + Sync + 'static> Body for Bound<S, H> {
409 fn observe<'a>(&'a mut self, ready: &'a (dyn Fn() + Send + Sync)) -> Step<'a> {
410 Box::pin(async move {
411 let name = self.identity.observer;
412 let activation = self.identity.activation;
413 let mut clock = interval(S::INTERVAL);
414 clock.set_missed_tick_behavior(MissedTickBehavior::Skip);
415
416 while self.observer.is_none() {
418 clock.tick().await;
419 match AssertUnwindSafe(S::initialize(&self.resources)).catch_unwind().await {
420 Ok(Ok(observer)) => self.observer = Some(observer),
421 Ok(Err(error)) => tracing::warn!(
422 observer = name, activation_id = %activation, error = %harmos_domain::failure::Report(&error),
423 "observer initialization failed; retrying next cadence"
424 ),
425 Err(_) => tracing::error!(
426 observer = name, activation_id = %activation,
427 "observer initialization panicked; retrying next cadence"
428 ),
429 }
430 }
431 ready();
432
433 let opened = Instant::now();
434 clock.reset();
435 let mut tick = Tick { ordinal: 0, elapsed: Duration::ZERO };
436 let observer = self.observer.as_mut().expect("initialized observer");
437 loop {
438 clock.tick().await;
439 advance_tick(&mut tick, opened.elapsed(), self.identity)?;
440 let context = ObserverContext {
441 identity: self.identity,
442 resources: self.resources.as_ref(),
443 tick: &tick,
444 host: &self.host,
445 extension: self.extension.as_deref(),
446 streams: S::STREAMS,
447 };
448 let observed = AssertUnwindSafe(async { observer.observe(&context).await }).catch_unwind().await;
449 observed_result(observed, self.identity, tick.ordinal())?;
450 }
451 })
452 }
453
454 fn stop(&mut self) -> Step<'_> {
455 Box::pin(async move {
456 let Some(observer) = self.observer.as_mut() else {
457 return Ok(());
458 };
459 if self.stopped {
460 return Ok(());
461 }
462 guarded(async { observer.stop().await }, ObserverError::StopFailed, ObserverError::StopPanicked).await?;
463 self.stopped = true;
464 Ok(())
465 })
466 }
467
468 fn cleanup(&mut self) -> Step<'_> {
469 Box::pin(async move {
470 let Some(observer) = self.observer.as_mut() else {
471 return Ok(());
472 };
473 guarded(async { observer.cleanup().await }, ObserverError::CleanupFailed, ObserverError::CleanupPanicked).await?;
474 self.observer = None;
475 Ok(())
476 })
477 }
478
479 fn is_initialized(&self) -> bool {
480 self.observer.is_some()
481 }
482}
483
484pub(super) fn validate_outputs<H>(instances: &[Instance<H>], grants: &[publications::Entry]) -> Result<(), String> {
487 let outputs = instances.iter().flat_map(|instance| instance.entry.streams);
488 let missing = |output: &&publications::Entry| !matches_output(output, grants);
489 let mut invalid = outputs.filter(missing);
490 match invalid.next() {
491 Some(output) => {
492 let message = format!("observer output `{}` differs from the artifact publication grant", output.id);
493 Err(message)
494 }
495 None => Ok(()),
496 }
497}
498
499fn observed_result<E: std::error::Error + 'static>(
502 observed: Result<Result<(), E>, Box<dyn Any + Send>>, identity: Identity, tick: u64,
503) -> Result<(), ObserverError> {
504 match observed {
505 Ok(Ok(())) => Ok(()),
506 Ok(Err(error)) => {
507 tracing::warn!(observer = identity.observer, activation_id = %identity.activation,
508 tick, error = %harmos_domain::failure::Report(&error), "observer callback refused");
509 Ok(())
510 }
511 Err(_) => {
512 tracing::error!(observer = identity.observer, activation_id = %identity.activation,
513 tick, "observer callback panicked; activation stopping");
514 Err(ObserverError::CallbackPanicked)
515 }
516 }
517}
518
519fn matches_output(output: &publications::Entry, grants: &[publications::Entry]) -> bool {
520 grants
521 .iter()
522 .any(|grant| grant.id == output.id && grant.schema == output.schema && grant.window == output.window && grant.row == output.row)
523}
524
525fn advance_tick(tick: &mut Tick, elapsed: Duration, identity: Identity) -> Result<(), ObserverError> {
526 let result = tick.advance(elapsed);
527 if result.is_err() {
528 tracing::error!(observer = identity.observer, activation_id = %identity.activation,
529 "observer tick ordinal exhausted; activation stopping");
530 }
531 result
532}
533
534async fn guarded<E: std::error::Error + Send + Sync + 'static>(
535 work: impl Future<Output = Result<(), E>>, refused: fn(Arc<dyn std::error::Error + Send + Sync>) -> ObserverError,
536 panicked: ObserverError,
537) -> Result<(), ObserverError> {
538 match AssertUnwindSafe(work).catch_unwind().await {
539 Ok(Ok(())) => Ok(()),
540 Ok(Err(source)) => {
541 let source = Arc::new(source);
542 let error = refused(source);
543 Err(error)
544 }
545 Err(_) => Err(panicked),
546 }
547}
548
549#[cfg(test)]
552mod tests {
553 use super::{Identity, ObserverContext, Tick};
554 use std::sync::Arc;
555 use std::time::Duration;
556
557 #[test]
558 fn context_resource_resolution() {
559 let value = Arc::new(17_u32);
560 let mut table = harmos_sdk::__private::WireTable::new("observer").unwrap();
561 table.provide_resource(Arc::clone(&value)).unwrap();
562 let resources = table.context();
563 let tick = Tick { ordinal: 1, elapsed: Duration::ZERO };
564 let context = ObserverContext {
565 identity: Identity::new("resource_observer").unwrap(),
566 resources: &resources,
567 tick: &tick,
568 host: &(),
569 streams: &[],
570 extension: None,
571 };
572
573 let resolved = context.resource::<u32>().unwrap();
574 assert!(Arc::ptr_eq(&value, &resolved));
575 assert!(context.resource::<u64>().is_err());
576 }
577}