bacnet_server/cov/
lifetime.rs1use super::*;
2use std::num::NonZeroU32;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CovTimeRemaining {
7 Indefinite,
9 Finite(NonZeroU32),
11 Expired,
13}
14
15impl CovTimeRemaining {
16 pub fn at(expires_at: Option<Instant>, now: Instant) -> Self {
19 let Some(expires_at) = expires_at else {
20 return Self::Indefinite;
21 };
22 let Some(remaining) = expires_at.checked_duration_since(now) else {
23 return Self::Expired;
24 };
25 if remaining.is_zero() {
26 return Self::Expired;
27 }
28 let seconds = remaining
29 .as_secs()
30 .saturating_add(u64::from(remaining.subsec_nanos() != 0));
31 Self::Finite(NonZeroU32::new(u32::try_from(seconds).unwrap_or(u32::MAX)).unwrap())
32 }
33
34 pub fn wire_seconds(self) -> Option<u32> {
36 match self {
37 Self::Indefinite => Some(0),
38 Self::Finite(seconds) => Some(seconds.get()),
39 Self::Expired => None,
40 }
41 }
42}
43
44impl CovSubscriptionTable {
45 pub fn remaining_lifetime(
48 &self,
49 snapshot: &CovSubscriptionSnapshot,
50 now: Instant,
51 ) -> Option<CovTimeRemaining> {
52 if !Arc::ptr_eq(&self.owner, &snapshot.owner) {
53 return None;
54 }
55 self.subs.get(snapshot.key()).and_then(|entry| {
56 (entry.generation == snapshot.generation && entry.flight.same(&snapshot.flight))
57 .then(|| CovTimeRemaining::at(entry.expires_at, now))
58 })
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use std::time::Duration;
66
67 #[test]
68 fn cov_lifetime_projection_distinguishes_finite_indefinite_and_expired() {
69 let now = Instant::now();
70 assert_eq!(
71 CovTimeRemaining::at(None, now),
72 CovTimeRemaining::Indefinite
73 );
74 assert_eq!(CovTimeRemaining::at(None, now).wire_seconds(), Some(0));
75 for (duration, expected) in [
76 (Duration::from_nanos(1), 1),
77 (Duration::from_millis(999), 1),
78 (Duration::from_secs(1), 1),
79 (Duration::from_millis(1001), 2),
80 (Duration::from_secs(3), 3),
81 (Duration::from_secs(u64::from(u32::MAX)), u32::MAX),
82 (Duration::from_secs(u64::from(u32::MAX) + 1), u32::MAX),
83 ] {
84 let projected = CovTimeRemaining::at(Some(now + duration), now);
85 assert_eq!(
86 projected,
87 CovTimeRemaining::Finite(NonZeroU32::new(expected).unwrap())
88 );
89 assert_eq!(projected.wire_seconds(), Some(expected));
90 }
91 for expiry in [now, now - Duration::from_nanos(1)] {
92 assert_eq!(
93 CovTimeRemaining::at(Some(expiry), now),
94 CovTimeRemaining::Expired
95 );
96 assert_eq!(CovTimeRemaining::at(Some(expiry), now).wire_seconds(), None);
97 }
98 }
99
100 #[test]
101 fn cov_lifetime_live_query_follows_context_refresh_and_rejects_stale_owners() {
102 let now = Instant::now();
103 let proposal = CovSubscription {
104 subscriber_mac: MacAddr::from_slice(&[1]),
105 subscriber_network: None,
106 subscriber_process_identifier: 1,
107 monitored_object_identifier: ObjectIdentifier::new(
108 bacnet_types::enums::ObjectType::ANALOG_VALUE,
109 1,
110 )
111 .unwrap(),
112 issue_confirmed_notifications: false,
113 expires_at: Some(now + Duration::from_secs(1)),
114 last_notified_observation: None,
115 monitored_property: Some(PropertyIdentifier::PRESENT_VALUE),
116 monitored_property_array_index: None,
117 cov_increment: None,
118 notification_kind: CovNotificationKind::Multiple,
119 timestamped: false,
120 };
121 let mut table = CovSubscriptionTable::new();
122 let snapshot = table.admit_for_test(proposal.clone(), 0).unwrap();
123 let context = snapshot.key().multiple_context().unwrap().clone();
124 table
125 .subscribe_multiple(
126 &context,
127 &snapshot.endpoint(),
128 now + Duration::from_secs(10),
129 0,
130 None,
131 vec![],
132 )
133 .unwrap();
134 assert_eq!(
135 table
136 .remaining_lifetime(&snapshot, now + Duration::from_secs(2))
137 .unwrap()
138 .wire_seconds(),
139 Some(8)
140 );
141 assert_eq!(
142 table.remaining_lifetime(&snapshot, now + Duration::from_secs(10)),
143 Some(CovTimeRemaining::Expired)
144 );
145 assert_eq!(snapshot.expires_at, proposal.expires_at);
146 let foreign = CovSubscriptionTable::new()
147 .admit_for_test(proposal.clone(), 0)
148 .unwrap();
149 assert_eq!(table.remaining_lifetime(&foreign, now), None);
150 table.admit_for_test(proposal, 0).unwrap();
151 assert_eq!(table.remaining_lifetime(&snapshot, now), None);
152 table.unsubscribe(snapshot.key());
153 assert_eq!(table.remaining_lifetime(&snapshot, now), None);
154 }
155}