Skip to main content

bacnet_server/cov/
admission.rs

1use super::*;
2
3/// A SubscribeCOVPropertyMultiple admission that did not take every reference
4/// (Clause 13.16.2). Processing runs in request order and stops at the first
5/// reference that fails; the references before it stay subscribed and are
6/// owed their initial notifications, and those after it are never looked at.
7#[derive(Debug)]
8pub struct MultipleRefusal {
9    /// The failure to report.
10    pub error: Error,
11    /// Position, counted from 0 in request order, of the reference that
12    /// failed; `None` when the request failed as a whole, before any
13    /// reference was processed.
14    pub refused: Option<usize>,
15    /// Snapshots of the references accepted before the failure (final
16    /// duplicates only, in request order); empty when nothing was kept.
17    pub committed: Vec<CovSubscriptionSnapshot>,
18}
19
20impl From<Error> for MultipleRefusal {
21    /// A failure of the whole request, which keeps nothing.
22    fn from(error: Error) -> Self {
23        Self {
24            error,
25            refused: None,
26            committed: Vec::new(),
27        }
28    }
29}
30
31/// The cap a new subscription would go past.
32#[derive(Clone, Copy)]
33enum Cap {
34    /// The recipient's own quota.
35    Peer,
36    /// The table's global or unreserved capacity.
37    Table,
38}
39
40impl CovSubscriptionTable {
41    /// Accept an ordinary or Single insertion or renewal after quota and
42    /// generation preflight. Multiple references are rejected before any table
43    /// effect: they enter only through [`subscribe_multiple`](Self::subscribe_multiple),
44    /// which owns their shared context lifetime and maximum notification delay.
45    pub fn subscribe(&mut self, sub: CovSubscription) -> Result<CovSubscriptionSnapshot, Error> {
46        let key = sub.key()?;
47        if key.multiple_context().is_some() {
48            return Err(Error::Encoding(
49                "Multiple references must be admitted through subscribe_multiple".into(),
50            ));
51        }
52        self.purge_expired();
53        let existing = self.subs.get(&key).cloned();
54        self.check_admission(
55            &sub.recipient(),
56            sub.expires_at.is_none(),
57            existing.as_deref(),
58        )?;
59        let generation = self.reserve_generations(1)?;
60        Ok(self.publish(key, sub, generation, None, None, None))
61    }
62
63    /// Accept Multiple references in request order and refresh their exact
64    /// context. All identities/options are validated before quota/generation
65    /// reservation or refresh. Each proposal that would add a subscription is
66    /// then checked against the recipient's quota and the table's capacity in
67    /// turn (a renewal, or a repeat of an earlier proposal, adds none), and
68    /// the first that does not fit ends the request (Clause 13.16.2, #1059).
69    /// The proposals before it are kept as if they had been the whole request
70    /// and come back in a [`MultipleRefusal`] naming its position; when it is
71    /// the first proposal, or the request fails as a whole (identity or route
72    /// mismatch, generation exhaustion), nothing changes (#1058). Final
73    /// duplicates among the kept proposals win, once each.
74    /// The request's expiry, maximum notification delay and the maximum APDU
75    /// its subscriber advertised become the whole context's (last write wins;
76    /// a `None` maximum APDU, unknown, keeps the one advertised before). The
77    /// delay bounds how long the context's
78    /// timestamped changes may stay queued after a notification failed or was
79    /// held back; notifications fit the smaller of the subscriber's maximum
80    /// APDU and the local one.
81    /// The admitted route also replaces the route of every retained reference,
82    /// including empty renewals. A changed route fences old snapshots while
83    /// preserving unreplaced observations; same-route refresh retains authority,
84    /// unless it lists references while the context's confirmed report is
85    /// outstanding or holding off, which fences it the same way (#896). A fence
86    /// while that report is outstanding, holding off or owed also clears the
87    /// observations of the kept untimestamped references it carried, since the
88    /// fenced report may have reached the subscriber (#923).
89    pub fn subscribe_multiple(
90        &mut self,
91        context: &MultipleContextKey,
92        route: &SubscriberEndpoint,
93        expires_at: Instant,
94        max_notification_delay: u32,
95        subscriber_max_apdu: Option<u16>,
96        mut subscriptions: Vec<CovSubscription>,
97    ) -> Result<Vec<CovSubscriptionSnapshot>, MultipleRefusal> {
98        context.recipient.validate()?;
99        let recipient = CovRecipient::from_endpoint(&route.mac, route.network.as_ref());
100        recipient.validate()?;
101        if recipient != context.recipient {
102            return Err(
103                Error::Encoding("Multiple route does not match its recipient".into()).into(),
104            );
105        }
106        for sub in &subscriptions {
107            if sub.key()?.multiple_context() != Some(context)
108                || sub.expires_at != Some(expires_at)
109                || sub.endpoint() != *route
110            {
111                return Err(Error::Encoding(
112                    "Multiple subscription does not match its context/route/expiry".into(),
113                )
114                .into());
115            }
116        }
117        self.purge_expired();
118        let overflow = self
119            .first_overflow(&recipient, &subscriptions)
120            .map(|(position, cap)| (position, self.refuse_new(cap)));
121        match overflow {
122            Some((0, error)) => {
123                return Err(MultipleRefusal {
124                    error,
125                    refused: Some(0),
126                    committed: Vec::new(),
127                });
128            }
129            Some((position, _)) => subscriptions.truncate(position),
130            None => {}
131        }
132        // Final options win exactly once, retaining the final-occurrence request order.
133        subscriptions.reverse();
134        let mut keys = std::collections::HashSet::new();
135        subscriptions.retain(|sub| keys.insert(sub.key().expect("validated identity")));
136        subscriptions.reverse();
137        let first_generation = self.reserve_generations(subscriptions.len())?;
138        // No fallible step follows this point. Unreplaced context references retain generations.
139        let (flight, replaced) = self.context_flight(context, route, !subscriptions.is_empty());
140        // A request whose header is unknown (the raw-data handler) keeps the
141        // maximum APDU the context's subscriber advertised before.
142        let subscriber_max_apdu = subscriber_max_apdu.or_else(|| {
143            self.subs
144                .values()
145                .find(|entry| entry.key.multiple_context() == Some(context))
146                .and_then(|entry| entry.subscriber_max_apdu)
147        });
148        let mut previously_indefinite = 0;
149        for entry in self.subs.values_mut() {
150            if entry.key.multiple_context() == Some(context) {
151                previously_indefinite += usize::from(entry.expires_at.is_none());
152                entry.subscription.expires_at = Some(expires_at);
153                entry.max_notification_delay = Some(max_notification_delay);
154                entry.subscriber_max_apdu = subscriber_max_apdu;
155                entry.subscription.subscriber_mac = route.mac.clone();
156                entry.subscription.subscriber_network = route.network.clone();
157                entry.flight = flight.clone();
158            }
159        }
160        if let Some(count) = self.peer_indefinite_counts.get_mut(&recipient) {
161            *count -= previously_indefinite;
162            if *count == 0 {
163                self.peer_indefinite_counts.remove(&recipient);
164            }
165        }
166        let accepted = subscriptions
167            .into_iter()
168            .enumerate()
169            .map(|(offset, sub)| {
170                self.publish(
171                    sub.key().expect("validated identity"),
172                    sub,
173                    first_generation + offset as u64,
174                    Some(max_notification_delay),
175                    subscriber_max_apdu,
176                    Some(flight.clone()),
177                )
178            })
179            .collect();
180        {
181            let mut timed = self.timed.lock();
182            timed.set_delay(context, max_notification_delay);
183            // Lifetime left now, rounded up as notifications report it; an
184            // expired one, which no notification reports, sizes as the longest.
185            let time_remaining = CovTimeRemaining::at(Some(expires_at), runtime_clock::now())
186                .wire_seconds()
187                .unwrap_or(u32::MAX);
188            timed.set_sizing(context, subscriber_max_apdu, time_remaining);
189        }
190        if let Some(replaced) = replaced {
191            self.fence_context_flight(context, &replaced, &keys);
192        }
193        match overflow {
194            None => Ok(accepted),
195            Some((position, error)) => Err(MultipleRefusal {
196                error,
197                refused: Some(position),
198                committed: accepted,
199            }),
200        }
201    }
202
203    /// The first proposal that would add a subscription past one of
204    /// `recipient`'s caps, and the cap it meets. Renewals of live entries and
205    /// repeats of an earlier proposal add none.
206    fn first_overflow(
207        &self,
208        recipient: &CovRecipient,
209        proposals: &[CovSubscription],
210    ) -> Option<(usize, Cap)> {
211        let (peer_room, table_room) = self.room(recipient);
212        let room = peer_room.min(table_room);
213        let mut added = std::collections::HashSet::new();
214        for (position, sub) in proposals.iter().enumerate() {
215            let key = sub.key().expect("validated identity");
216            if self.subs.contains_key(&key) || added.contains(&key) {
217                continue;
218            }
219            if added.len() == room {
220                let cap = self
221                    .cap_passed(recipient, room + 1)
222                    .expect("one past the room passes a cap");
223                return Some((position, cap));
224            }
225            added.insert(key);
226        }
227        None
228    }
229
230    /// Test fixture: admit one proposal through its family's production
231    /// owner. A Multiple reference is a one-reference `subscribe_multiple`
232    /// request (finite expiry required) that refreshes its exact context with
233    /// the supplied maximum notification delay.
234    #[cfg(test)]
235    pub(crate) fn admit_for_test(
236        &mut self,
237        sub: CovSubscription,
238        max_notification_delay: u32,
239    ) -> Result<CovSubscriptionSnapshot, Error> {
240        let Some(context) = sub.key()?.multiple_context().cloned() else {
241            return self.subscribe(sub);
242        };
243        let expires_at = sub
244            .expires_at
245            .expect("Multiple contexts always have a finite lifetime");
246        let mut accepted = self
247            .subscribe_multiple(
248                &context,
249                &sub.endpoint(),
250                expires_at,
251                max_notification_delay,
252                None,
253                vec![sub],
254            )
255            .map_err(|refusal| refusal.error)?;
256        Ok(accepted.remove(0))
257    }
258
259    fn reserve_generations(&mut self, count: usize) -> Result<u64, Error> {
260        if count == 0 {
261            return Ok(self.generation);
262        }
263        let last = u64::try_from(count)
264            .ok()
265            .and_then(|count| self.generation.checked_add(count));
266        let Some(last) = last else {
267            self.counters
268                .subscriptions_rejected_capacity
269                .fetch_add(1, Ordering::Relaxed);
270            return Err(Error::protocol(
271                ErrorClass::RESOURCES,
272                ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT,
273            ));
274        };
275        let first = self.generation + 1;
276        self.generation = last;
277        Ok(first)
278    }
279
280    fn publish(
281        &mut self,
282        key: CovSubscriptionKey,
283        sub: CovSubscription,
284        generation: u64,
285        max_notification_delay: Option<u32>,
286        subscriber_max_apdu: Option<u16>,
287        context_flight: Option<super::confirmed::FlightMarker>,
288    ) -> CovSubscriptionSnapshot {
289        let snapshot = CovSubscriptionSnapshot {
290            key,
291            generation,
292            owner: Arc::clone(&self.owner),
293            last_successful_ticket: 0,
294            flight: context_flight.unwrap_or_default(),
295            subscription: sub.clone(),
296            max_notification_delay,
297            subscriber_max_apdu,
298        };
299        {
300            let mut timed = self.timed.lock();
301            let delay = max_notification_delay.unwrap_or_default();
302            match (snapshot.key.multiple_context(), sub.timestamped) {
303                (Some(_), true) => timed.reset(&snapshot.key, generation, delay),
304                (Some(_), false) => timed.reset_untimed(&snapshot.key, generation, delay),
305                (None, _) => timed.remove(&snapshot.key),
306            }
307        }
308        let peer = sub.recipient();
309        let new_indefinite = sub.expires_at.is_none();
310        match self.subs.insert(snapshot.key.clone(), snapshot.clone()) {
311            Some(old) => {
312                if !old.flight.same(&snapshot.flight) {
313                    // The replaced incarnation's outstanding report stops retrying;
314                    // the replacement reports for itself (#896).
315                    old.flight.fence();
316                }
317                let old_indefinite = old.expires_at.is_none();
318                if old_indefinite != new_indefinite {
319                    if new_indefinite {
320                        *self.peer_indefinite_counts.entry(peer).or_default() += 1;
321                    } else if let Some(count) = self.peer_indefinite_counts.get_mut(&peer) {
322                        *count = count.saturating_sub(1);
323                        if *count == 0 {
324                            self.peer_indefinite_counts.remove(&peer);
325                        }
326                    }
327                }
328            }
329            _ => {
330                *self.peer_counts.entry(peer.clone()).or_default() += 1;
331                if new_indefinite {
332                    *self.peer_indefinite_counts.entry(peer).or_default() += 1;
333                }
334                self.counters
335                    .subscriptions_created
336                    .fetch_add(1, Ordering::Relaxed);
337            }
338        }
339        self.counters
340            .subscriptions_active
341            .store(self.subs.len() as u64, Ordering::Relaxed);
342        snapshot
343    }
344
345    /// Check admission for a single subscription request against policy quotas.
346    pub(super) fn check_admission(
347        &mut self,
348        peer: &CovRecipient,
349        is_indefinite: bool,
350        existing: Option<&CovSubscription>,
351    ) -> Result<(), Error> {
352        self.purge_expired();
353
354        if is_indefinite && !self.policy.allow_indefinite_subscriptions {
355            self.counters
356                .subscriptions_rejected_indefinite
357                .fetch_add(1, Ordering::Relaxed);
358            return Err(Error::protocol(
359                ErrorClass::SERVICES,
360                ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
361            ));
362        }
363
364        let (new_count, new_indefinite) = match existing {
365            Some(sub) => {
366                let was_indefinite = sub.expires_at.is_none();
367                let add_indefinite = if is_indefinite && !was_indefinite {
368                    1
369                } else {
370                    0
371                };
372                (0, add_indefinite)
373            }
374            None => {
375                let add_indefinite = if is_indefinite { 1 } else { 0 };
376                (1, add_indefinite)
377            }
378        };
379
380        self.check_caps(peer, new_count, new_indefinite)
381    }
382
383    /// Check the indefinite policy and the subscription caps for one
384    /// admission adding `new_count` subscriptions, `new_indefinite` of them
385    /// indefinite.
386    fn check_caps(
387        &mut self,
388        peer: &CovRecipient,
389        new_count: usize,
390        new_indefinite: usize,
391    ) -> Result<(), Error> {
392        self.purge_expired();
393
394        if new_indefinite > 0 {
395            if !self.policy.allow_indefinite_subscriptions {
396                self.counters
397                    .subscriptions_rejected_indefinite
398                    .fetch_add(1, Ordering::Relaxed);
399                return Err(Error::protocol(
400                    ErrorClass::SERVICES,
401                    ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
402                ));
403            }
404            let current_indefinite = self.peer_indefinite_counts.get(peer).copied().unwrap_or(0);
405            if current_indefinite + new_indefinite > self.policy.max_indefinite_per_peer {
406                self.counters
407                    .subscriptions_rejected_indefinite
408                    .fetch_add(1, Ordering::Relaxed);
409                return Err(Error::protocol(
410                    ErrorClass::RESOURCES,
411                    ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT,
412                ));
413            }
414        }
415
416        match self.cap_passed(peer, new_count) {
417            Some(cap) => Err(self.refuse_new(cap)),
418            None => Ok(()),
419        }
420    }
421
422    /// How many more subscriptions `peer` may add under its own quota, and
423    /// under the table's capacity (the global cap and, for a recipient
424    /// without a reservation, the unreserved share).
425    fn room(&self, peer: &CovRecipient) -> (usize, usize) {
426        let current_peer = self.peer_counts.get(peer).copied().unwrap_or(0);
427        let peer_room = self
428            .policy
429            .max_subscriptions_per_peer
430            .saturating_sub(current_peer);
431        let mut capacity = self.policy.max_subscriptions_global;
432        if !self.policy.is_peer_reserved(peer) {
433            capacity = capacity.min(self.policy.effective_unreserved_capacity());
434        }
435        (peer_room, capacity.saturating_sub(self.subs.len()))
436    }
437
438    /// The cap `new_count` more subscriptions of `peer` would pass, its own
439    /// quota checked first.
440    fn cap_passed(&self, peer: &CovRecipient, new_count: usize) -> Option<Cap> {
441        let (peer_room, table_room) = self.room(peer);
442        if new_count > peer_room {
443            Some(Cap::Peer)
444        } else if new_count > table_room {
445            Some(Cap::Table)
446        } else {
447            None
448        }
449    }
450
451    /// Count a refusal at `cap` and build its error.
452    fn refuse_new(&self, cap: Cap) -> Error {
453        let counter = match cap {
454            Cap::Peer => &self.counters.subscriptions_rejected_quota,
455            Cap::Table => &self.counters.subscriptions_rejected_capacity,
456        };
457        counter.fetch_add(1, Ordering::Relaxed);
458        Error::protocol(
459            ErrorClass::RESOURCES,
460            ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT,
461        )
462    }
463}