1use std::collections::{HashMap, HashSet};
2
3use bacnet_objects::database::ObjectDatabase;
4use bacnet_objects::event::{
5 EventStateChange, EventTransition, EventTransitionCommit, EventTransitionCommitError,
6};
7use bacnet_objects::event_enrollment::EventEnrollmentReliabilityCommit;
8use bacnet_objects::event_enrollment::{EventEnrollmentEvalState, EventEnrollmentMonitoredSource};
9use bacnet_types::enums::{EventState, EventType, PropertyIdentifier, Reliability};
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
11
12use super::EventEnrollmentTransition;
13use crate::server::event_notification_payload::{
14 CapturedReferencedValue, CommittedNotificationPayload, EventEnrollmentProjectionSnapshot,
15 project_event_enrollment_payload,
16};
17use crate::server::event_timestamp::{
18 SampledEventClock, confirm_event_timestamp, stage_event_timestamp,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EventEnrollmentEvaluationStage {
24 Evaluation,
26 EvaluationSource,
28 EvaluationState,
30 Reliability,
32 EventTransition,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum EventEnrollmentEvaluationOutcome {
39 NoTransition,
41 CancellationCommitted,
43 ObservationUnavailable,
50 Rejected,
52 LandedAfterError,
58}
59
60impl EventEnrollmentEvaluationOutcome {
61 pub fn is_failure(self) -> bool {
63 matches!(self, Self::Rejected | Self::LandedAfterError)
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct EventEnrollmentEvaluationDiagnostic {
70 pub enrollment_oid: ObjectIdentifier,
72 pub stage: EventEnrollmentEvaluationStage,
74 pub outcome: EventEnrollmentEvaluationOutcome,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum EventEnrollmentReliabilityCause {
81 Configuration,
83 MonitoredObject,
85 FaultAlgorithm,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct EventEnrollmentReliabilityResult {
92 pub enrollment_oid: ObjectIdentifier,
94 pub monitored_oid: Option<ObjectIdentifier>,
96 pub previous_reliability: Reliability,
98 pub new_reliability: Reliability,
100 pub state_change: Option<EventStateChange>,
102 pub distribute: bool,
104 pub cause: EventEnrollmentReliabilityCause,
106}
107
108impl EventEnrollmentReliabilityResult {
109 pub fn event_type(&self, configured_algorithm: EventType) -> Option<EventType> {
114 self.state_change
115 .as_ref()
116 .map(|change| change.event_type(configured_algorithm))
117 }
118}
119
120#[derive(Debug, Clone, Default, PartialEq)]
122pub struct EventEnrollmentEvaluationReport {
123 pub transitions: Vec<EventEnrollmentTransition>,
125 pub reliability_results: Vec<EventEnrollmentReliabilityResult>,
127 pub diagnostics: Vec<EventEnrollmentEvaluationDiagnostic>,
129}
130
131pub(crate) enum CommittedEventEnrollmentResult {
133 Normal(EventEnrollmentTransition),
134 Reliability(EventEnrollmentReliabilityResult),
135}
136
137pub(crate) struct CommittedEventEnrollmentDelivery {
139 pub(crate) result: CommittedEventEnrollmentResult,
140 pub(crate) ack_required: bool,
141 pub(crate) recipient_clock: SampledEventClock,
142 pub(crate) event_type: EventType,
143 pub(crate) event_values: CommittedNotificationPayload,
144}
145
146pub(crate) struct EventEnrollmentEvaluationBatch {
148 pub(crate) report: EventEnrollmentEvaluationReport,
149 pub(crate) deliveries: Vec<CommittedEventEnrollmentDelivery>,
150}
151
152pub(crate) fn log_evaluation_report(report: &EventEnrollmentEvaluationReport) {
153 for result in &report.reliability_results {
154 tracing::debug!(
155 enrollment = %result.enrollment_oid,
156 monitored = ?result.monitored_oid,
157 previous = ?result.previous_reliability,
158 new = ?result.new_reliability,
159 state_change = ?result.state_change,
160 distribute = result.distribute,
161 cause = ?result.cause,
162 "Event enrollment: reliability committed"
163 );
164 }
165 for diagnostic in report
166 .diagnostics
167 .iter()
168 .filter(|diagnostic| diagnostic.outcome.is_failure())
169 {
170 tracing::warn!(
171 enrollment = %diagnostic.enrollment_oid,
172 stage = ?diagnostic.stage,
173 outcome = ?diagnostic.outcome,
174 "Event enrollment: internal commit failed"
175 );
176 }
177}
178
179#[derive(Default)]
181pub(super) struct EnrollmentUpdate {
182 eval_state: Option<EventEnrollmentEvalState>,
183 eval_source: Option<Option<EventEnrollmentMonitoredSource>>,
184 clears_invalidation: bool,
185 fired: Option<FiredTransition>,
186 reliability: Option<ReliabilityUpdate>,
187 observation_unavailable: bool,
188 canceled: bool,
189}
190
191pub(super) struct FiredTransition {
192 pub(super) monitored_oid: ObjectIdentifier,
193 pub(super) event_type: EventType,
194 pub(super) from: EventState,
195 pub(super) to: EventState,
196 pub(super) distribute: bool,
197 pub(super) ack_required: bool,
198 pub(super) projection: EventEnrollmentProjectionSnapshot,
199}
200
201pub(super) struct ReliabilityUpdate {
202 pub(super) monitored_oid: Option<ObjectIdentifier>,
203 pub(super) previous: Reliability,
204 pub(super) desired: Reliability,
205 pub(super) from: EventState,
206 pub(super) to: EventState,
207 pub(super) distribute: bool,
208 pub(super) ack_required: bool,
209 pub(super) cause: EventEnrollmentReliabilityCause,
210 pub(super) referenced_value: CapturedReferencedValue,
211}
212
213impl EnrollmentUpdate {
214 pub(super) fn set_eval_state(&mut self, state: EventEnrollmentEvalState) {
218 self.eval_state = Some(state);
219 }
220
221 pub(super) fn reset_eval_state(&mut self) {
222 self.eval_state = Some(EventEnrollmentEvalState::default());
223 self.clears_invalidation = true;
224 }
225
226 pub(super) fn cancel_pending(&mut self, state: EventEnrollmentEvalState) {
227 self.eval_state = Some(state);
228 self.canceled = true;
229 }
230
231 pub(super) fn set_eval_source(&mut self, source: Option<EventEnrollmentMonitoredSource>) {
232 self.eval_source = Some(source);
233 }
234
235 pub(super) fn reset_eval_source_for_observation_gap(
238 &mut self,
239 previous: Option<Option<EventEnrollmentMonitoredSource>>,
240 ) {
241 self.eval_source = match previous {
242 Some(Some(_)) => Some(None),
243 Some(None) | None => None,
244 };
245 }
246
247 pub(super) fn fire(&mut self, fired: FiredTransition) {
248 debug_assert!(
249 self.fired.is_none(),
250 "one enrollment fires at most once per pass"
251 );
252 self.fired = Some(fired);
253 }
254
255 pub(super) fn set_reliability(&mut self, reliability: ReliabilityUpdate) {
256 debug_assert!(
257 self.reliability.is_none() && self.fired.is_none(),
258 "one enrollment commits one transition family per pass"
259 );
260 self.reliability = Some(reliability);
261 }
262
263 pub(super) fn observation_unavailable(&mut self) {
264 self.fired = None;
267 self.reliability = None;
268 self.canceled = false;
269 self.observation_unavailable = true;
270 }
271}
272
273fn diagnostic(
274 enrollment_oid: ObjectIdentifier,
275 stage: EventEnrollmentEvaluationStage,
276 outcome: EventEnrollmentEvaluationOutcome,
277) -> EventEnrollmentEvaluationDiagnostic {
278 EventEnrollmentEvaluationDiagnostic {
279 enrollment_oid,
280 stage,
281 outcome,
282 }
283}
284
285fn event_state_landed(db: &ObjectDatabase, oid: &ObjectIdentifier, state: EventState) -> bool {
286 matches!(
287 db.get(oid)
288 .and_then(|object| object.read_property(PropertyIdentifier::EVENT_STATE, None).ok()),
289 Some(PropertyValue::Enumerated(raw)) if raw == state.to_raw()
290 )
291}
292
293fn reliability_or_state_landed(
294 db: &ObjectDatabase,
295 oid: &ObjectIdentifier,
296 update: &ReliabilityUpdate,
297) -> bool {
298 let Some(object) = db.get(oid) else {
299 return false;
300 };
301 let reliability_landed = update.previous != update.desired
302 && matches!(
303 object.read_property(PropertyIdentifier::RELIABILITY, None),
304 Ok(PropertyValue::Enumerated(raw)) if raw == update.desired.to_raw()
305 );
306 let state_landed = update.from != update.to
307 && matches!(
308 object.read_property(PropertyIdentifier::EVENT_STATE, None),
309 Ok(PropertyValue::Enumerated(raw)) if raw == update.to.to_raw()
310 );
311 reliability_landed || state_landed
312}
313
314fn clear_source_ownership(
315 db: &mut ObjectDatabase,
316 oid: ObjectIdentifier,
317 database_eval_sources: &HashSet<ObjectIdentifier>,
318) {
319 if database_eval_sources.contains(&oid) {
320 db.set_enrollment_eval_source(oid, None);
321 } else if let Some(object) = db.get_mut(&oid) {
322 let _ = object.set_enrollment_eval_source_internal(None);
323 }
324}
325
326#[cfg(test)]
327pub(super) fn apply_updates(
328 db: &mut ObjectDatabase,
329 oids: &[ObjectIdentifier],
330 updates: HashMap<ObjectIdentifier, EnrollmentUpdate>,
331 database_eval_sources: &HashSet<ObjectIdentifier>,
332) -> EventEnrollmentEvaluationReport {
333 apply_updates_for_delivery(db, oids, updates, database_eval_sources).report
334}
335
336pub(super) fn apply_updates_for_delivery(
337 db: &mut ObjectDatabase,
338 oids: &[ObjectIdentifier],
339 mut updates: HashMap<ObjectIdentifier, EnrollmentUpdate>,
340 database_eval_sources: &HashSet<ObjectIdentifier>,
341) -> EventEnrollmentEvaluationBatch {
342 let mut report = EventEnrollmentEvaluationReport::default();
343 let mut deliveries = Vec::new();
344
345 for &oid in oids {
346 let Some(update) = updates.remove(&oid) else {
347 report.diagnostics.push(diagnostic(
348 oid,
349 EventEnrollmentEvaluationStage::Evaluation,
350 EventEnrollmentEvaluationOutcome::NoTransition,
351 ));
352 continue;
353 };
354
355 let mut source_failed = false;
356 let mut state_failed = false;
357 if let Some(source) = update.eval_source {
358 if database_eval_sources.contains(&oid) {
359 db.set_enrollment_eval_source(oid, source);
360 } else {
361 let failed = db.get_mut(&oid).is_none_or(|object| {
362 object.set_enrollment_eval_source_internal(source).is_err()
363 });
364 if failed {
365 source_failed = true;
366 report.diagnostics.push(diagnostic(
367 oid,
368 EventEnrollmentEvaluationStage::EvaluationSource,
369 EventEnrollmentEvaluationOutcome::Rejected,
370 ));
371 }
372 }
373 }
374
375 let mut state_committed = false;
376 if source_failed {
377 state_failed = db.get_mut(&oid).is_none_or(|object| {
380 object
381 .set_enrollment_eval_state_internal(EventEnrollmentEvalState::default())
382 .is_err()
383 });
384 if state_failed {
385 db.set_enrollment_eval_state_invalidated(oid, true);
386 report.diagnostics.push(diagnostic(
387 oid,
388 EventEnrollmentEvaluationStage::EvaluationState,
389 EventEnrollmentEvaluationOutcome::Rejected,
390 ));
391 } else {
392 state_committed = true;
393 }
394 } else if let Some(state) = update.eval_state {
395 state_failed = db
396 .get_mut(&oid)
397 .is_none_or(|object| object.set_enrollment_eval_state_internal(state).is_err());
398 if state_failed {
399 clear_source_ownership(db, oid, database_eval_sources);
400 db.set_enrollment_eval_state_invalidated(oid, true);
401 report.diagnostics.push(diagnostic(
402 oid,
403 EventEnrollmentEvaluationStage::EvaluationState,
404 EventEnrollmentEvaluationOutcome::Rejected,
405 ));
406 } else {
407 state_committed = true;
408 if update.clears_invalidation {
409 db.set_enrollment_eval_state_invalidated(oid, false);
410 }
411 }
412 }
413
414 if update.observation_unavailable {
415 if source_failed || state_failed {
416 db.set_enrollment_eval_state_invalidated(oid, true);
417 }
418 report.diagnostics.push(diagnostic(
419 oid,
420 EventEnrollmentEvaluationStage::Reliability,
421 EventEnrollmentEvaluationOutcome::ObservationUnavailable,
422 ));
423 continue;
424 }
425
426 if state_failed {
427 continue;
428 }
429
430 if let Some(reliability) = update.reliability {
431 let coordinate = EventTransition::for_target_state(reliability.to);
432 let staged_timestamp = stage_event_timestamp(db);
433 let change = EventStateChange {
434 from: reliability.from,
435 to: reliability.to,
436 };
437 let transition = EventTransitionCommit {
438 change: change.clone(),
439 coordinate,
440 ack_required: reliability.ack_required,
441 timestamp: staged_timestamp.sample.timestamp.clone(),
442 message_text: Some(String::new()),
446 };
447 let commit = EventEnrollmentReliabilityCommit {
448 reliability: reliability.desired,
449 transition: Some(transition),
450 };
451 let commit_result = db
452 .get_mut(&oid)
453 .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
454 object.commit_event_enrollment_reliability_internal(commit)
455 });
456
457 if commit_result.is_err() {
458 let outcome = if reliability_or_state_landed(db, &oid, &reliability) {
459 EventEnrollmentEvaluationOutcome::LandedAfterError
460 } else {
461 EventEnrollmentEvaluationOutcome::Rejected
462 };
463 clear_source_ownership(db, oid, database_eval_sources);
464 db.set_enrollment_eval_state_invalidated(oid, true);
465 report.diagnostics.push(diagnostic(
466 oid,
467 EventEnrollmentEvaluationStage::Reliability,
468 outcome,
469 ));
470 continue;
471 }
472
473 let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
474 let result = EventEnrollmentReliabilityResult {
475 enrollment_oid: oid,
476 monitored_oid: reliability.monitored_oid,
477 previous_reliability: reliability.previous,
478 new_reliability: reliability.desired,
479 state_change: Some(change),
480 distribute: reliability.distribute,
481 cause: reliability.cause,
482 };
483 report.reliability_results.push(result.clone());
484 let event_type = EventType::CHANGE_OF_RELIABILITY;
485 if let Some(event_values) = project_event_enrollment_payload(
486 db,
487 oid,
488 reliability.monitored_oid,
489 result
490 .state_change
491 .as_ref()
492 .expect("committed Reliability delivery has a state change"),
493 event_type,
494 None,
495 Some(&reliability.referenced_value),
496 ) {
497 deliveries.push(CommittedEventEnrollmentDelivery {
498 result: CommittedEventEnrollmentResult::Reliability(result),
499 ack_required: reliability.ack_required,
500 recipient_clock,
501 event_type,
502 event_values,
503 });
504 } else {
505 tracing::debug!(
506 enrollment = %oid,
507 "Committed Event Enrollment reliability payload rejected; suppressing distribution"
508 );
509 }
510 continue;
511 }
512
513 let Some(fired) = update.fired else {
514 report.diagnostics.push(diagnostic(
515 oid,
516 EventEnrollmentEvaluationStage::EvaluationState,
517 if update.canceled && state_committed {
518 EventEnrollmentEvaluationOutcome::CancellationCommitted
519 } else {
520 EventEnrollmentEvaluationOutcome::NoTransition
521 },
522 ));
523 continue;
524 };
525
526 if source_failed && fired.from == fired.to {
531 continue;
532 }
533
534 let coordinate = EventTransition::for_target_state(fired.to);
535 let staged_timestamp = stage_event_timestamp(db);
536 let change = EventStateChange {
537 from: fired.from,
538 to: fired.to,
539 };
540 let commit = EventTransitionCommit {
541 change: change.clone(),
542 coordinate,
543 ack_required: fired.ack_required,
544 timestamp: staged_timestamp.sample.timestamp.clone(),
545 message_text: Some(String::new()),
547 };
548 let commit_result = db
549 .get_mut(&oid)
550 .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
551 object.commit_event_transition_internal(commit)
552 });
553
554 if commit_result.is_err() {
555 let outcome = if fired.from != fired.to && event_state_landed(db, &oid, fired.to) {
556 EventEnrollmentEvaluationOutcome::LandedAfterError
557 } else {
558 EventEnrollmentEvaluationOutcome::Rejected
559 };
560 clear_source_ownership(db, oid, database_eval_sources);
561 db.set_enrollment_eval_state_invalidated(oid, true);
562 report.diagnostics.push(diagnostic(
563 oid,
564 EventEnrollmentEvaluationStage::EventTransition,
565 outcome,
566 ));
567 continue;
568 }
569
570 let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
571 let event_type = change.event_type(fired.event_type);
572 let result = EventEnrollmentTransition {
573 enrollment_oid: oid,
574 monitored_oid: fired.monitored_oid,
575 change,
576 event_type,
577 distribute: fired.distribute,
578 };
579 report.transitions.push(result.clone());
580 if let Some(event_values) = project_event_enrollment_payload(
581 db,
582 oid,
583 Some(fired.monitored_oid),
584 &result.change,
585 event_type,
586 Some(&fired.projection),
587 None,
588 ) {
589 deliveries.push(CommittedEventEnrollmentDelivery {
590 result: CommittedEventEnrollmentResult::Normal(result),
591 ack_required: fired.ack_required,
592 recipient_clock,
593 event_type,
594 event_values,
595 });
596 } else {
597 tracing::debug!(
598 enrollment = %oid,
599 ?event_type,
600 "Committed Event Enrollment payload rejected; suppressing distribution"
601 );
602 }
603 }
604
605 EventEnrollmentEvaluationBatch { report, deliveries }
606}