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 project_event_enrollment_payload, CapturedReferencedValue, CommittedNotificationPayload,
15 EventEnrollmentProjectionSnapshot,
16};
17use crate::server::event_timestamp::{
18 confirm_event_timestamp, stage_event_timestamp, SampledEventClock,
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: None,
443 };
444 let commit = EventEnrollmentReliabilityCommit {
445 reliability: reliability.desired,
446 transition: Some(transition),
447 };
448 let commit_result = db
449 .get_mut(&oid)
450 .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
451 object.commit_event_enrollment_reliability_internal(commit)
452 });
453
454 if commit_result.is_err() {
455 let outcome = if reliability_or_state_landed(db, &oid, &reliability) {
456 EventEnrollmentEvaluationOutcome::LandedAfterError
457 } else {
458 EventEnrollmentEvaluationOutcome::Rejected
459 };
460 clear_source_ownership(db, oid, database_eval_sources);
461 db.set_enrollment_eval_state_invalidated(oid, true);
462 report.diagnostics.push(diagnostic(
463 oid,
464 EventEnrollmentEvaluationStage::Reliability,
465 outcome,
466 ));
467 continue;
468 }
469
470 let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
471 let result = EventEnrollmentReliabilityResult {
472 enrollment_oid: oid,
473 monitored_oid: reliability.monitored_oid,
474 previous_reliability: reliability.previous,
475 new_reliability: reliability.desired,
476 state_change: Some(change),
477 distribute: reliability.distribute,
478 cause: reliability.cause,
479 };
480 report.reliability_results.push(result.clone());
481 let event_type = EventType::CHANGE_OF_RELIABILITY;
482 if let Some(event_values) = project_event_enrollment_payload(
483 db,
484 oid,
485 reliability.monitored_oid,
486 result
487 .state_change
488 .as_ref()
489 .expect("committed Reliability delivery has a state change"),
490 event_type,
491 None,
492 Some(&reliability.referenced_value),
493 ) {
494 deliveries.push(CommittedEventEnrollmentDelivery {
495 result: CommittedEventEnrollmentResult::Reliability(result),
496 ack_required: reliability.ack_required,
497 recipient_clock,
498 event_type,
499 event_values,
500 });
501 } else {
502 tracing::debug!(
503 enrollment = %oid,
504 "Committed Event Enrollment reliability payload rejected; suppressing distribution"
505 );
506 }
507 continue;
508 }
509
510 let Some(fired) = update.fired else {
511 report.diagnostics.push(diagnostic(
512 oid,
513 EventEnrollmentEvaluationStage::EvaluationState,
514 if update.canceled && state_committed {
515 EventEnrollmentEvaluationOutcome::CancellationCommitted
516 } else {
517 EventEnrollmentEvaluationOutcome::NoTransition
518 },
519 ));
520 continue;
521 };
522
523 if source_failed && fired.from == fired.to {
528 continue;
529 }
530
531 let coordinate = EventTransition::for_target_state(fired.to);
532 let staged_timestamp = stage_event_timestamp(db);
533 let change = EventStateChange {
534 from: fired.from,
535 to: fired.to,
536 };
537 let commit = EventTransitionCommit {
538 change: change.clone(),
539 coordinate,
540 ack_required: fired.ack_required,
541 timestamp: staged_timestamp.sample.timestamp.clone(),
542 message_text: None,
543 };
544 let commit_result = db
545 .get_mut(&oid)
546 .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
547 object.commit_event_transition_internal(commit)
548 });
549
550 if commit_result.is_err() {
551 let outcome = if fired.from != fired.to && event_state_landed(db, &oid, fired.to) {
552 EventEnrollmentEvaluationOutcome::LandedAfterError
553 } else {
554 EventEnrollmentEvaluationOutcome::Rejected
555 };
556 clear_source_ownership(db, oid, database_eval_sources);
557 db.set_enrollment_eval_state_invalidated(oid, true);
558 report.diagnostics.push(diagnostic(
559 oid,
560 EventEnrollmentEvaluationStage::EventTransition,
561 outcome,
562 ));
563 continue;
564 }
565
566 let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
567 let event_type = change.event_type(fired.event_type);
568 let result = EventEnrollmentTransition {
569 enrollment_oid: oid,
570 monitored_oid: fired.monitored_oid,
571 change,
572 event_type,
573 distribute: fired.distribute,
574 };
575 report.transitions.push(result.clone());
576 if let Some(event_values) = project_event_enrollment_payload(
577 db,
578 oid,
579 Some(fired.monitored_oid),
580 &result.change,
581 event_type,
582 Some(&fired.projection),
583 None,
584 ) {
585 deliveries.push(CommittedEventEnrollmentDelivery {
586 result: CommittedEventEnrollmentResult::Normal(result),
587 ack_required: fired.ack_required,
588 recipient_clock,
589 event_type,
590 event_values,
591 });
592 } else {
593 tracing::debug!(
594 enrollment = %oid,
595 ?event_type,
596 "Committed Event Enrollment payload rejected; suppressing distribution"
597 );
598 }
599 }
600
601 EventEnrollmentEvaluationBatch { report, deliveries }
602}