Skip to main content

bacnet_client/
log_reader.rs

1//! Paged reads of a log's Log_Buffer over ReadRange (#1530).
2//!
3//! [`read_log_page`] reads one page from a cursor and returns the cursor to
4//! read from next, so a caller loops until the page says it is done and
5//! keeps the last cursor as its checkpoint. One request is outstanding at a
6//! time. On a [`BACnetClient`] the client's minimum request interval paces
7//! them, and on the endpoint client the session's does (#1542).
8//!
9//! Sequence numbers are the log's own: a log numbers each record with its
10//! Total_Record_Count once the record is added, so the newest record carries
11//! Total_Record_Count and the oldest that number less Record_Count, plus
12//! one. The numbers count up to the top of their Unsigned32 range
13//! (Unsigned64 for an Audit Log) and carry on from 1, never 0 (Clauses
14//! 12.25.14 and 12.25.16).
15
16use core::future::Future;
17
18use bacnet_encoding::constructed::tagged::{decode_app_unsigned, expect_end};
19use bacnet_services::read_range::{
20    LogRecords, RangeSpec, ReadRangeReply, ReadRangeRequest, ReadRangeValidation,
21    ReadRangeViolation,
22};
23use bacnet_transport::port::TransportPort;
24use bacnet_types::enums::{ObjectType, PropertyIdentifier};
25use bacnet_types::error::Error;
26use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
27
28use crate::client::BACnetClient;
29
30/// Where a paged log read starts, or resumes from a checkpoint.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum LogCursor {
33    /// The oldest record the log holds, found from its Record_Count and
34    /// Total_Record_Count; the read goes on by sequence number.
35    Oldest,
36    /// The record with this sequence number, then on by sequence number.
37    Sequence(u64),
38    /// The record at this one-based position, then on by position. The
39    /// fallback for a device whose sequence numbers are inconsistent, which
40    /// fails a sequence read with [`Error::LogNotAdvancing`]. Not for a log
41    /// that is merely full: each record it drops shifts every position, so a
42    /// position read of a busy full log skips records without a gap.
43    Position(u64),
44    /// The first record newer than this date and time, which must be
45    /// specific; the read goes on by sequence number.
46    Time(Date, Time),
47}
48
49/// A page whose first record is not the one the read asked for: the log no
50/// longer holds the records in between.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct LogGap {
53    /// The sequence number the read asked for.
54    pub expected: u64,
55    /// The sequence number of the page's first record.
56    pub first: u64,
57    /// How many records were lost between the two; `None` when the log's
58    /// numbering went back below `expected`, as when its Total_Record_Count
59    /// was reset.
60    pub skipped: Option<u64>,
61}
62
63/// One page of a paged log read.
64#[derive(Debug, Clone)]
65pub struct LogPage {
66    /// The page's records, oldest first.
67    pub records: LogRecords,
68    /// The first record's sequence number; `None` for a page read by
69    /// position, or an empty page.
70    pub first_sequence_number: Option<u64>,
71    /// FIRST_ITEM, LAST_ITEM and MORE_ITEMS as the device set them; all
72    /// false for a page the reader made up for an empty log.
73    pub result_flags: (bool, bool, bool),
74    /// Set when the first record is past the one the read asked for.
75    pub gap: Option<LogGap>,
76    /// Rules the device's answer broke that the reader tolerates, such as a
77    /// first sequence number of 0 after the device's wrap.
78    pub violations: Vec<ReadRangeViolation>,
79    /// Where to read next: the next page while `done` is false, else the
80    /// checkpoint to resume from once the log has more records.
81    pub next: LogCursor,
82    /// The page reached the newest record (LAST_ITEM), or found none.
83    pub done: bool,
84    /// The page's records reach the top of the sequence range, so `next`
85    /// wrapped to 1. A device that numbers the record after the top 0
86    /// rather than 1 (against the standard) is then one number ahead of
87    /// `next`; see [`read_log_page`].
88    pub wrapped: bool,
89}
90
91/// What a paged log read needs from a requester: a property read as an
92/// Unsigned, and a ReadRange checked leniently, so the reader decides which
93/// broken rules to tolerate.
94pub trait LogRequester {
95    /// Read `property` of `log` at `mac` as an Unsigned.
96    fn read_unsigned(
97        &self,
98        mac: &[u8],
99        log: ObjectIdentifier,
100        property: PropertyIdentifier,
101    ) -> impl Future<Output = Result<u64, Error>> + Send;
102
103    /// Send one ReadRange to `mac` and check its answer leniently.
104    fn read_range_lenient(
105        &self,
106        mac: &[u8],
107        request: &ReadRangeRequest,
108    ) -> impl Future<Output = Result<ReadRangeReply, Error>> + Send;
109}
110
111impl<T: TransportPort + 'static> LogRequester for BACnetClient<T> {
112    async fn read_unsigned(
113        &self,
114        mac: &[u8],
115        log: ObjectIdentifier,
116        property: PropertyIdentifier,
117    ) -> Result<u64, Error> {
118        let ack = self.read_property(mac, log, property, None).await?;
119        unsigned_value(&ack.property_value)
120    }
121
122    async fn read_range_lenient(
123        &self,
124        mac: &[u8],
125        request: &ReadRangeRequest,
126    ) -> Result<ReadRangeReply, Error> {
127        self.read_range_with(mac, request, ReadRangeValidation::Lenient)
128            .await
129    }
130}
131
132/// An application-tagged Unsigned that fills `value`.
133pub fn unsigned_value(value: &[u8]) -> Result<u64, Error> {
134    const WHAT: &str = "log record count";
135    let (count, end) = decode_app_unsigned::<u64>(value, 0, WHAT)?;
136    expect_end(value, end, end, WHAT)?;
137    Ok(count)
138}
139
140impl<T: TransportPort + 'static> BACnetClient<T> {
141    /// Read one page of `log`'s Log_Buffer from `cursor`, asking for up to
142    /// `page_size` records (1..=32767); see [`read_log_page`].
143    ///
144    /// ```no_run
145    /// # use bacnet_client::client::BACnetClient;
146    /// # use bacnet_client::log_reader::LogCursor;
147    /// # use bacnet_transport::bip::BipTransport;
148    /// # async fn read_all(
149    /// #     client: &BACnetClient<BipTransport>,
150    /// #     mac: &[u8],
151    /// #     log: bacnet_types::primitives::ObjectIdentifier,
152    /// # ) -> Result<LogCursor, bacnet_types::error::Error> {
153    /// let mut cursor = LogCursor::Oldest;
154    /// loop {
155    ///     let page = client.read_log_page(mac, log, cursor, 100).await?;
156    ///     // ... store page.records ...
157    ///     cursor = page.next;
158    ///     if page.done {
159    ///         return Ok(cursor); // persist as the checkpoint
160    ///     }
161    /// }
162    /// # }
163    /// ```
164    pub async fn read_log_page(
165        &self,
166        mac: &[u8],
167        log: ObjectIdentifier,
168        cursor: LogCursor,
169        page_size: u16,
170    ) -> Result<LogPage, Error> {
171        read_log_page(self, mac, log, cursor, page_size).await
172    }
173}
174
175/// The sequence numbers a log of one type assigns: 1 up to `max`, then 1
176/// again.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub(crate) struct SequenceSpace {
179    max: u64,
180}
181
182/// The records a log holds now, from its two counts.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub(crate) enum Window {
185    /// No records; the next one will carry `next`.
186    Empty { next: u64 },
187    /// Records from `oldest` to `newest`.
188    Records { oldest: u64, newest: u64 },
189}
190
191impl SequenceSpace {
192    /// The space of `object_type`'s log, or an error when it keeps no log.
193    pub(crate) fn of(object_type: ObjectType) -> Result<Self, Error> {
194        match object_type {
195            ObjectType::AUDIT_LOG => Ok(Self { max: u64::MAX }),
196            ObjectType::TREND_LOG | ObjectType::EVENT_LOG | ObjectType::TREND_LOG_MULTIPLE => {
197                Ok(Self {
198                    max: u64::from(u32::MAX),
199                })
200            }
201            other => Err(Error::Encoding(format!(
202                "a paged log read needs a Trend Log, Event Log, Trend Log Multiple or \
203                 Audit Log, not {other}"
204            ))),
205        }
206    }
207
208    /// `sequence` moved on by `n` records. A device that numbered a record
209    /// 0 after its wrap counts on from it in its own numbering.
210    pub(crate) fn advance(self, sequence: u64, n: u64) -> u64 {
211        if sequence == 0 {
212            return n;
213        }
214        let size = u128::from(self.max);
215        ((u128::from(sequence - 1) + u128::from(n)) % size + 1) as u64
216    }
217
218    /// The sequence number `back` records before `sequence`. After a
219    /// Total_Record_Count of 0, the device counts through 0, so the
220    /// arithmetic does too.
221    fn retreat(self, sequence: u64, back: u64) -> u64 {
222        if sequence == 0 {
223            let size = u128::from(self.max) + 1;
224            return ((size - u128::from(back) % size) % size) as u64;
225        }
226        let size = u128::from(self.max);
227        ((u128::from(sequence - 1) + size - u128::from(back) % size) % size + 1) as u64
228    }
229
230    /// How far `to` lies past `from`: positive ahead, negative behind,
231    /// across the wrap. 0, a device's number for the record after its wrap,
232    /// counts as 1.
233    pub(crate) fn distance(self, from: u64, to: u64) -> i128 {
234        let size = i128::from(self.max);
235        let at = |sequence: u64| i128::from(sequence.max(1));
236        let ahead = (at(to) - at(from)).rem_euclid(size);
237        if ahead > size / 2 {
238            ahead - size
239        } else {
240            ahead
241        }
242    }
243
244    /// The records a log holds, from its Record_Count and
245    /// Total_Record_Count.
246    pub(crate) fn window(self, record_count: u64, total: u64) -> Result<Window, Error> {
247        if record_count > self.max || total > self.max {
248            return Err(Error::OutOfRange(format!(
249                "log counts {record_count} and {total} exceed the log's sequence range"
250            )));
251        }
252        if record_count == 0 {
253            return Ok(Window::Empty {
254                next: self.advance(total, 1),
255            });
256        }
257        Ok(Window::Records {
258            oldest: self.retreat(total, record_count - 1),
259            newest: total,
260        })
261    }
262}
263
264/// The ReadRange count for `page_size`.
265fn page_count(page_size: u16) -> Result<i32, Error> {
266    match i16::try_from(page_size) {
267        Ok(count @ 1..) => Ok(i32::from(count)),
268        _ => Err(Error::Encoding(format!(
269            "log page size must be 1..=32767, got {page_size}"
270        ))),
271    }
272}
273
274fn request(log: ObjectIdentifier, cursor: LogCursor, count: i32) -> ReadRangeRequest {
275    let range = match cursor {
276        LogCursor::Oldest => unreachable!("Oldest resolves to a sequence number first"),
277        LogCursor::Sequence(reference_seq) => RangeSpec::BySequenceNumber {
278            reference_seq,
279            count,
280        },
281        LogCursor::Position(reference_index) => RangeSpec::ByPosition {
282            reference_index,
283            count,
284        },
285        LogCursor::Time(date, time) => RangeSpec::ByTime {
286            reference_time: (date, time),
287            count,
288        },
289    };
290    ReadRangeRequest {
291        object_identifier: log,
292        property_identifier: PropertyIdentifier::LOG_BUFFER,
293        property_array_index: None,
294        range: Some(range),
295    }
296}
297
298/// Most times a read reads the counts again because the log moved between
299/// two requests, or starts over from the oldest record because the log
300/// dropped it meanwhile.
301const RETRIES: usize = 3;
302
303/// The records `log` holds now.
304///
305/// Total_Record_Count is read on both sides of Record_Count: a record logged
306/// in between would otherwise make the oldest look one newer than it is, and
307/// the read would skip it without a gap. When the total moves, the counts are
308/// read again, up to [`RETRIES`] times; after that the count goes with the
309/// total read before it, which can only make the oldest look older. On a log
310/// that isn't full that may report a gap no record fell into, but no record is
311/// ever skipped without one; the empty-page path in [`read_log_page`]
312/// recovers from a number the log doesn't hold.
313async fn window<R: LogRequester + ?Sized>(
314    requester: &R,
315    mac: &[u8],
316    log: ObjectIdentifier,
317    space: SequenceSpace,
318) -> Result<Window, Error> {
319    let total = || requester.read_unsigned(mac, log, PropertyIdentifier::TOTAL_RECORD_COUNT);
320    let mut before = total().await?;
321    let mut attempts = 0;
322    loop {
323        let record_count = requester
324            .read_unsigned(mac, log, PropertyIdentifier::RECORD_COUNT)
325            .await?;
326        let after = total().await?;
327        attempts += 1;
328        if after == before || attempts > RETRIES {
329            return space.window(record_count, before);
330        }
331        before = after;
332    }
333}
334
335/// Read one page of `log`'s Log_Buffer from `cursor` through `requester`,
336/// asking for up to `page_size` records (1..=32767).
337///
338/// - [`LogCursor::Oldest`] reads Total_Record_Count, Record_Count and
339///   Total_Record_Count again first to find the oldest record.
340/// - Each page asks for `page_size` records from the cursor; the next page
341///   starts at the first sequence number plus the records returned, across
342///   the wrap. MORE_ITEMS only says the answer was cut short to fit, so any
343///   page that isn't the last continues; LAST_ITEM or an empty page ends.
344/// - A page whose first record is past the one asked for reports a
345///   [`LogGap`]. An empty page by sequence number reads the counts again:
346///   past the newest record the read is done; when the log holds the record
347///   asked for (logged since), the read asks again; when it no longer holds
348///   it, the read starts over from the oldest it does hold, with a gap. A
349///   full log can drop that one too before it is read, so a read starts over
350///   up to three times.
351/// - A page that starts before the record asked for fails with
352///   [`Error::LogNotAdvancing`] instead of repeating records. So does an
353///   empty answer for a record the counts still say the log holds, once the
354///   read has asked again for a record logged since, or after any restart
355///   from the oldest; and a log that drops its oldest record faster than the
356///   restarts catch it. A device that does either because its sequence
357///   numbers are inconsistent can be read from [`LogCursor::Position`].
358/// - The answer is checked leniently: a first sequence number of 0 (a
359///   device's number for the record after its wrap), a first sequence number
360///   on a page by position, contradictory flags and a count overrun are
361///   tolerated and listed in [`LogPage::violations`]. Any other broken rule
362///   fails with [`Error::ReadRangeViolation`].
363/// - A page whose records don't decode fails with [`Error::Decoding`] naming
364///   the first that fails; the records before it are dropped. Read that range
365///   with `read_range_with` and decode it with
366///   [`ReadRangeAck::log_records`](bacnet_services::read_range::ReadRangeAck::log_records)
367///   to keep them.
368///
369/// The standard numbers the record after the top of the range 1. A device
370/// that numbers it 0 instead is one number ahead of this reader past its
371/// wrap: a page that ends at or crosses the top ([`LogPage::wrapped`]) sets
372/// `next` to 1, which that device gave the record after its 0. Reading on
373/// loses that 0 record without a gap; resuming a caught-up log from such a
374/// checkpoint can instead start over from the oldest record without
375/// reporting a skip. Such devices usually also clamp a reference past the
376/// wrap, which fails as [`Error::LogNotAdvancing`].
377pub async fn read_log_page<R: LogRequester + ?Sized>(
378    requester: &R,
379    mac: &[u8],
380    log: ObjectIdentifier,
381    cursor: LogCursor,
382    page_size: u16,
383) -> Result<LogPage, Error> {
384    let space = SequenceSpace::of(log.object_type())?;
385    let count = page_count(page_size)?;
386    let cursor = match cursor {
387        LogCursor::Oldest => match window(requester, mac, log, space).await? {
388            Window::Empty { next } => return Ok(empty_page(log, LogCursor::Sequence(next))),
389            Window::Records { oldest, .. } => LogCursor::Sequence(oldest),
390        },
391        cursor => cursor,
392    };
393    let mut reply = requester
394        .read_range_lenient(mac, &request(log, cursor, count))
395        .await?;
396    let LogCursor::Sequence(expected) = cursor else {
397        return page(space, cursor, reply, None);
398    };
399    // Nothing from `asked` on: the counts tell whether the log ends there,
400    // has logged it since, or no longer holds it.
401    let mut asked = expected;
402    let mut asked_while_held = false;
403    let mut restarts = 0;
404    loop {
405        if reply.ack.item_count != 0 {
406            return page(
407                space,
408                LogCursor::Sequence(asked),
409                reply,
410                (asked != expected).then_some(expected),
411            );
412        }
413        if let Some(rule) = fatal(&reply.violations, LogCursor::Sequence(asked)) {
414            return Err(Error::ReadRangeViolation(rule));
415        }
416        if restarts == RETRIES {
417            break;
418        }
419        restarts += 1;
420        asked = match window(requester, mac, log, space).await? {
421            Window::Records { oldest, newest }
422                if space.distance(asked, space.advance(newest, 1)) != 0 =>
423            {
424                let held = space.distance(oldest, asked) >= 0 && space.distance(asked, newest) >= 0;
425                if held && asked_while_held {
426                    break;
427                }
428                // Either way the record asked for next is one the counts
429                // say the log holds.
430                asked_while_held = true;
431                if held { asked } else { oldest }
432            }
433            _ => return page(space, LogCursor::Sequence(asked), reply, None),
434        };
435        reply = requester
436            .read_range_lenient(mac, &request(log, LogCursor::Sequence(asked), count))
437            .await?;
438    }
439    Err(Error::LogNotAdvancing {
440        requested: asked,
441        returned: None,
442    })
443}
444
445/// A page with no records, which reads from `next` once the log has any.
446fn empty_page(log: ObjectIdentifier, next: LogCursor) -> LogPage {
447    LogPage {
448        records: LogRecords::empty_for(log.object_type())
449            .expect("a log type was checked before reading"),
450        first_sequence_number: None,
451        result_flags: (false, false, false),
452        gap: None,
453        violations: Vec::new(),
454        next,
455        done: true,
456        wrapped: false,
457    }
458}
459
460/// The first rule in `violations` that a read from `cursor` can't
461/// tolerate: an echo that doesn't match, or a page read by sequence number
462/// or time without its first sequence number.
463fn fatal(violations: &[ReadRangeViolation], cursor: LogCursor) -> Option<ReadRangeViolation> {
464    let sequenced = !matches!(cursor, LogCursor::Position(_));
465    violations
466        .iter()
467        .copied()
468        .find(|violation| match violation {
469            ReadRangeViolation::MissingFirstSequenceNumber => sequenced,
470            ReadRangeViolation::ZeroFirstSequenceNumber
471            | ReadRangeViolation::UnexpectedFirstSequenceNumber
472            | ReadRangeViolation::MoreItemsPastEnd
473            | ReadRangeViolation::ItemCountExceedsRequest => false,
474            _ => true,
475        })
476}
477
478/// Assemble the page `reply` answers to a read from `cursor`; `restarted`
479/// is the sequence number the caller asked for when the read had to start
480/// over from the oldest record.
481pub(crate) fn page(
482    space: SequenceSpace,
483    cursor: LogCursor,
484    reply: ReadRangeReply,
485    restarted: Option<u64>,
486) -> Result<LogPage, Error> {
487    let ReadRangeReply { ack, violations } = reply;
488    if let Some(rule) = fatal(&violations, cursor) {
489        return Err(Error::ReadRangeViolation(rule));
490    }
491    let records = match ack.log_records() {
492        Some(records) => records?,
493        None => {
494            return Err(Error::ReadRangeViolation(
495                ReadRangeViolation::PropertyMismatch,
496            ));
497        }
498    };
499    let returned = u64::from(ack.item_count);
500    let done = returned == 0 || ack.result_flags.1;
501    let mut gap = None;
502    let mut wrapped = false;
503    let (first_sequence_number, next) = match (cursor, ack.first_sequence_number) {
504        (LogCursor::Position(position), _) => {
505            (None, LogCursor::Position(position.saturating_add(returned)))
506        }
507        (cursor, None) => (None, cursor),
508        (cursor, Some(first)) => {
509            if let LogCursor::Sequence(asked) = cursor {
510                let ahead = space.distance(asked, first);
511                if ahead < 0 {
512                    return Err(Error::LogNotAdvancing {
513                        requested: asked,
514                        returned: Some(first),
515                    });
516                }
517                let expected = restarted.unwrap_or(asked);
518                let skipped = space.distance(expected, first);
519                if skipped != 0 {
520                    gap = Some(LogGap {
521                        expected,
522                        first,
523                        skipped: u64::try_from(skipped).ok(),
524                    });
525                }
526            }
527            // The records run from `first` to `first + returned - 1`: they
528            // reach the top of the range when that passes `max`.
529            wrapped =
530                first != 0 && u128::from(first) + u128::from(returned) > u128::from(space.max);
531            (
532                Some(first),
533                LogCursor::Sequence(space.advance(first, returned)),
534            )
535        }
536    };
537    Ok(LogPage {
538        records,
539        first_sequence_number,
540        result_flags: ack.result_flags,
541        gap,
542        violations,
543        next,
544        done,
545        wrapped,
546    })
547}
548
549#[cfg(test)]
550#[path = "log_reader_tests.rs"]
551mod tests;
552
553#[cfg(test)]
554#[path = "log_reader_device_tests.rs"]
555mod device_tests;