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;