Skip to content

Commit ff51f29

Browse files
authored
[Rust] Encapsulated HLC Error logic (#596)
Modularized HLC errors, and made them appropriate for user API
1 parent 9c5c20f commit ff51f29

File tree

5 files changed

+138
-116
lines changed

5 files changed

+138
-116
lines changed

rust/azure_iot_operations_protocol/src/application.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ use std::{
88
time::Duration,
99
};
1010

11-
use crate::common::{
12-
aio_protocol_error::AIOProtocolError,
13-
hybrid_logical_clock::{HybridLogicalClock, DEFAULT_MAX_CLOCK_DRIFT},
14-
};
11+
use crate::common::hybrid_logical_clock::{HLCError, HybridLogicalClock, DEFAULT_MAX_CLOCK_DRIFT};
1512

1613
/// Struct containing the application-level [`HybridLogicalClock`].
1714
pub struct ApplicationHybridLogicalClock {
@@ -47,13 +44,13 @@ impl ApplicationHybridLogicalClock {
4744
/// other [`HybridLogicalClock`], and the current time, and its counter will also be updated accordingly.
4845
///
4946
/// # Errors
50-
/// [`AIOProtocolError`] of kind [`InternalLogicError`](crate::common::aio_protocol_error::AIOProtocolErrorKind::InternalLogicError) if
47+
/// [`HLCError`] of kind [`OverflowWarning`](crate::common::hybrid_logical_clock::HLCErrorKind::OverflowWarning) if
5148
/// the [`ApplicationHybridLogicalClock`]'s counter would be set to a value that would overflow beyond [`u64::MAX`]
5249
///
53-
/// [`AIOProtocolError`] of kind [`StateInvalid`](crate::common::aio_protocol_error::AIOProtocolErrorKind::StateInvalid) if
50+
/// [`HLCError`] of kind [`ClockDrift`](crate::common::hybrid_logical_clock::HLCErrorKind::ClockDrift) if
5451
/// the latest [`HybridLogicalClock`] (of [`ApplicationHybridLogicalClock`] or `other`)'s timestamp is too far in
5552
/// the future (determined by [`max_clock_drift`](ApplicationHybridLogicalClock::max_clock_drift)) compared to `SystemTime::now()`
56-
pub(crate) fn update(&self, other_hlc: &HybridLogicalClock) -> Result<(), AIOProtocolError> {
53+
pub(crate) fn update(&self, other_hlc: &HybridLogicalClock) -> Result<(), HLCError> {
5754
self.hlc
5855
.lock()
5956
.unwrap()
@@ -63,13 +60,13 @@ impl ApplicationHybridLogicalClock {
6360
/// Updates the [`ApplicationHybridLogicalClock`] with the current time and returns a `String` representation of the updated [`ApplicationHybridLogicalClock`].
6461
///
6562
/// # Errors
66-
/// [`AIOProtocolError`] of kind [`InternalLogicError`](crate::common::aio_protocol_error::AIOProtocolErrorKind::InternalLogicError) if
63+
/// [`HLCError`] of kind [`OverflowWarning`](crate::common::hybrid_logical_clock::HLCErrorKind::OverflowWarning) if
6764
/// the [`HybridLogicalClock`]'s counter would be incremented and overflow beyond [`u64::MAX`]
6865
///
69-
/// [`AIOProtocolError`] of kind [`StateInvalid`](crate::common::aio_protocol_error::AIOProtocolErrorKind::StateInvalid) if
66+
/// [`HLCError`] of kind [`ClockDrift`](crate::common::hybrid_logical_clock::HLCErrorKind::ClockDrift) if
7067
/// the [`ApplicationHybridLogicalClock`]'s timestamp is too far in the future (determined
7168
/// by [`max_clock_drift`](ApplicationHybridLogicalClock::max_clock_drift)) compared to `SystemTime::now()`
72-
pub(crate) fn update_now(&self) -> Result<String, AIOProtocolError> {
69+
pub(crate) fn update_now(&self) -> Result<String, HLCError> {
7370
let mut hlc = self.hlc.lock().unwrap();
7471
hlc.update_now(self.max_clock_drift)?;
7572
Ok(hlc.to_string())

rust/azure_iot_operations_protocol/src/common/aio_protocol_error.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use std::error::Error;
55
use std::fmt;
66
use std::time::Duration;
77

8-
use crate::common::topic_processor::{TopicPatternError, TopicPatternErrorKind};
8+
use crate::common::{
9+
hybrid_logical_clock::{HLCError, HLCErrorKind, ParseHLCError},
10+
topic_processor::{TopicPatternError, TopicPatternErrorKind},
11+
};
912

1013
/// Represents the kind of error that occurs in an Azure IoT Operations Protocol
1114
#[derive(Debug, PartialEq)]
@@ -573,3 +576,36 @@ impl AIOProtocolError {
573576
}
574577
}
575578
}
579+
580+
impl From<HLCError> for AIOProtocolError {
581+
fn from(error: HLCError) -> Self {
582+
let (property_name, message) = match error.kind() {
583+
HLCErrorKind::OverflowWarning => {
584+
("Counter", "Integer overflow on HybridLogicalClock counter")
585+
}
586+
HLCErrorKind::ClockDrift => (
587+
"MaxClockDrift",
588+
"HybridLogicalClock drift is greater than the maximum allowed drift",
589+
),
590+
};
591+
592+
AIOProtocolError::new_state_invalid_error(
593+
property_name,
594+
None,
595+
Some(message.to_string()),
596+
None,
597+
)
598+
}
599+
}
600+
601+
impl From<ParseHLCError> for AIOProtocolError {
602+
fn from(error: ParseHLCError) -> Self {
603+
AIOProtocolError::new_header_invalid_error(
604+
"HybridLogicalClock",
605+
error.input.as_str(),
606+
false,
607+
Some(format!("{error}")),
608+
None,
609+
)
610+
}
611+
}

rust/azure_iot_operations_protocol/src/common/hybrid_logical_clock.rs

Lines changed: 80 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ use std::{
77
time::{Duration, SystemTime, UNIX_EPOCH},
88
};
99

10+
use thiserror::Error;
1011
use uuid::Uuid;
1112

12-
use super::aio_protocol_error::AIOProtocolError;
13-
1413
/// Recommended default value for max clock drift if not specified.
1514
pub const DEFAULT_MAX_CLOCK_DRIFT: Duration = Duration::from_secs(60);
1615

@@ -52,17 +51,17 @@ impl HybridLogicalClock {
5251
/// is a no-op, and will not result in an error.
5352
///
5453
/// # Errors
55-
/// [`AIOProtocolError`] of kind [`InternalLogicError`](crate::common::aio_protocol_error::AIOProtocolErrorKind::InternalLogicError) if
54+
/// [`HLCError`] of kind [`OverflowWarning`](HLCErrorKind::OverflowWarning) if
5655
/// the [`HybridLogicalClock`]'s counter would be set to a value that would overflow beyond [`u64::MAX`]
5756
///
58-
/// [`AIOProtocolError`] of kind [`StateInvalid`](crate::common::aio_protocol_error::AIOProtocolErrorKind::StateInvalid) if
59-
/// the latest [`HybridLogicalClock`] (of `Self` or `other`)'s timestamp is too far in the future (determined by `max_clock_drift`)
57+
/// [`HLCError`] of kind [`ClockDrift`](HLCErrorKind::ClockDrift) if the latest [`HybridLogicalClock`]
58+
/// (of `Self` or `other`)'s timestamp is too far in the future (determined by `max_clock_drift`)
6059
/// compared to [`SystemTime::now()`]
6160
pub fn update(
6261
&mut self,
6362
other: &HybridLogicalClock,
6463
max_clock_drift: Duration,
65-
) -> Result<(), AIOProtocolError> {
64+
) -> Result<(), HLCError> {
6665
let now = now_ms_precision();
6766
// Don't update from the same node.
6867
if self.node_id == other.node_id {
@@ -103,12 +102,13 @@ impl HybridLogicalClock {
103102
/// Updates the [`HybridLogicalClock`] based on the current time
104103
///
105104
/// # Errors
106-
/// [`AIOProtocolError`] of kind [`InternalLogicError`](crate::common::aio_protocol_error::AIOProtocolErrorKind::InternalLogicError) if
107-
/// the [`HybridLogicalClock`]'s counter would be incremented and overflow beyond [`u64::MAX`]
105+
/// [`HLCError`] of kind [`OverflowWarning`](HLCErrorKind::OverflowWarning) if
106+
/// the [`HybridLogicalClock`]'s counter would be set to a value that would overflow beyond [`u64::MAX`]
108107
///
109-
/// [`AIOProtocolError`] of kind [`StateInvalid`](crate::common::aio_protocol_error::AIOProtocolErrorKind::StateInvalid) if
110-
/// the [`HybridLogicalClock`]'s timestamp is too far in the future (determined by `max_clock_drift`) compared to [`SystemTime::now()`]
111-
pub fn update_now(&mut self, max_clock_drift: Duration) -> Result<(), AIOProtocolError> {
108+
/// [`HLCError`] of kind [`ClockDrift`](HLCErrorKind::ClockDrift) if the [`HybridLogicalClock`]
109+
/// timestamp is too far in the future (determined by `max_clock_drift`) compared to [`SystemTime::now()`]
110+
/// compared to [`SystemTime::now()`]
111+
pub fn update_now(&mut self, max_clock_drift: Duration) -> Result<(), HLCError> {
112112
let now = now_ms_precision();
113113

114114
// if now later than self, set the time to that and reset the counter
@@ -126,34 +126,18 @@ impl HybridLogicalClock {
126126
/// and that the counter will not overflow if it is increased.
127127
///
128128
/// # Errors
129-
/// [`AIOProtocolError`] of kind [`InternalLogicError`](crate::common::aio_protocol_error::AIOProtocolErrorKind::InternalLogicError)
130-
/// if the [`HybridLogicalClock`] counter is at [`u64::MAX`]
129+
/// [`HLCError`] of kind [`OverflowWarning`](HLCErrorKind::OverflowWarning) if
130+
/// the [`HybridLogicalClock`]'s counter would be set to a value that would overflow beyond [`u64::MAX`]
131131
///
132-
/// [`AIOProtocolError`] of kind [`StateInvalid`](crate::common::aio_protocol_error::AIOProtocolErrorKind::StateInvalid)
133-
/// if the [`HybridLogicalClock`]'s timestamp is too far in the future (determined by `max_clock_drift`)
134-
fn validate(&self, now: SystemTime, max_clock_drift: Duration) -> Result<(), AIOProtocolError> {
132+
/// [`HLCError`] of kind [`ClockDrift`](HLCErrorKind::ClockDrift) if the [`HybridLogicalClock`]
133+
/// timestamp is too far in the future (determined by `max_clock_drift`) compared to [`SystemTime::now()`]
134+
fn validate(&self, now: SystemTime, max_clock_drift: Duration) -> Result<(), HLCError> {
135135
if self.counter == u64::MAX {
136-
return Err(AIOProtocolError::new_internal_logic_error(
137-
true,
138-
false,
139-
None,
140-
"Counter",
141-
None,
142-
Some("Integer overflow on HybridLogicalClock counter".to_string()),
143-
None,
144-
));
136+
return Err(HLCErrorKind::OverflowWarning)?;
145137
}
146138
if let Ok(diff) = self.timestamp.duration_since(now) {
147139
if diff > max_clock_drift {
148-
return Err(AIOProtocolError::new_state_invalid_error(
149-
"MaxClockDrift",
150-
None,
151-
Some(
152-
"HybridLogicalClock drift is greater than the maximum allowed drift"
153-
.to_string(),
154-
),
155-
None,
156-
));
140+
return Err(HLCErrorKind::ClockDrift)?;
157141
}
158142
} // else negative time difference is ok, we only care if the HLC is too far in the future
159143

@@ -177,58 +161,46 @@ impl Display for HybridLogicalClock {
177161
}
178162

179163
impl FromStr for HybridLogicalClock {
180-
type Err = AIOProtocolError;
164+
type Err = ParseHLCError;
181165

182-
fn from_str(s: &str) -> Result<Self, AIOProtocolError> {
166+
fn from_str(s: &str) -> Result<Self, ParseHLCError> {
183167
let parts: Vec<&str> = s.split(':').collect();
184168
if parts.len() != 3 {
185-
return Err(AIOProtocolError::new_header_invalid_error(
186-
"HybridLogicalClock",
187-
s,
188-
false,
189-
None,
190-
None,
191-
));
169+
return Err(ParseHLCError {
170+
message: "Incorrect format".to_string(),
171+
input: s.to_string(),
172+
});
192173
}
193174

194175
// Validate first part (timestamp)
195176
let ms_since_epoch = match parts[0].parse::<u64>() {
196177
Ok(ms) => ms,
197178
Err(e) => {
198-
return Err(AIOProtocolError::new_header_invalid_error(
199-
"HybridLogicalClock",
200-
s,
201-
false,
202-
Some(format!(
179+
return Err(ParseHLCError {
180+
message: format!(
203181
"Malformed HLC. Could not parse first segment as an integer: {e}"
204-
)),
205-
None,
206-
));
182+
),
183+
input: s.to_string(),
184+
})
207185
}
208186
};
209187
let Some(timestamp) = UNIX_EPOCH.checked_add(Duration::from_millis(ms_since_epoch)) else {
210-
return Err(AIOProtocolError::new_header_invalid_error(
211-
"HybridLogicalClock",
212-
s,
213-
false,
214-
Some("Malformed HLC. Timestamp is out of range.".to_string()),
215-
None,
216-
));
188+
return Err(ParseHLCError {
189+
message: "Malformed HLC. Timestamp is out of range.".to_string(),
190+
input: s.to_string(),
191+
});
217192
};
218193

219194
// Validate second part (counter)
220195
let counter = match parts[1].parse::<u64>() {
221196
Ok(val) => val,
222197
Err(e) => {
223-
return Err(AIOProtocolError::new_header_invalid_error(
224-
"HybridLogicalClock",
225-
s,
226-
false,
227-
Some(format!(
198+
return Err(ParseHLCError {
199+
message: format!(
228200
"Malformed HLC. Could not parse second segment as an integer: {e}"
229-
)),
230-
None,
231-
));
201+
),
202+
input: s.to_string(),
203+
});
232204
}
233205
};
234206

@@ -278,6 +250,41 @@ fn now_ms_precision() -> SystemTime {
278250
now
279251
}
280252

253+
/// Represents errors that occur in the use of an HLC
254+
#[derive(Debug, Error)]
255+
#[error("{0}")]
256+
pub struct HLCError(#[from] HLCErrorKind);
257+
258+
impl HLCError {
259+
/// Returns the corresponding [`HLCErrorKind`] for this error
260+
#[must_use]
261+
pub fn kind(&self) -> &HLCErrorKind {
262+
&self.0
263+
}
264+
}
265+
266+
/// A list specifying categories of HLC error
267+
#[derive(Debug, Error)]
268+
pub enum HLCErrorKind {
269+
/// The counter would be incremented to a value that would overflow beyond [`u64::MAX`]
270+
#[error("counter cannot be incremented")]
271+
OverflowWarning,
272+
/// The HLC's timestamp is too far in the future compared to the current time
273+
#[error("exceeds max clock drift")]
274+
ClockDrift,
275+
}
276+
277+
/// Represents errors that occur when parsing an HLC from a string
278+
#[derive(Debug, Error)]
279+
#[error("{message}")]
280+
pub struct ParseHLCError {
281+
/// The error message
282+
message: String,
283+
/// The input string that failed to parse
284+
// NOTE: This is only needed for AIOProtocolError compatibility
285+
pub input: String,
286+
}
287+
281288
// Functions to allow manipulation of the system time for testing purposes
282289
#[cfg(test)]
283290
use std::cell::Cell;
@@ -296,12 +303,7 @@ fn set_time_offset(offset: Duration, positive: bool) {
296303

297304
#[cfg(test)]
298305
mod tests {
299-
use crate::common::{
300-
aio_protocol_error::AIOProtocolErrorKind,
301-
hybrid_logical_clock::{
302-
now_ms_precision, set_time_offset, HybridLogicalClock, DEFAULT_MAX_CLOCK_DRIFT,
303-
},
304-
};
306+
use super::*;
305307
use std::time::{Duration, UNIX_EPOCH};
306308
use test_case::test_case;
307309
use uuid::Uuid;
@@ -330,9 +332,7 @@ mod tests {
330332
Ok(()) => assert!(should_succeed),
331333
Err(e) => {
332334
assert!(!should_succeed);
333-
assert_eq!(e.kind, AIOProtocolErrorKind::StateInvalid);
334-
assert_eq!(e.property_name, Some("MaxClockDrift".to_string()));
335-
assert_eq!(e.property_value, None);
335+
matches!(e.kind(), HLCErrorKind::ClockDrift);
336336
}
337337
}
338338
}
@@ -362,9 +362,7 @@ mod tests {
362362
match hlc.validate(now_ms_precision(), max_drift) {
363363
Ok(()) => panic!("Expected error"),
364364
Err(e) => {
365-
assert_eq!(e.kind, AIOProtocolErrorKind::StateInvalid);
366-
assert_eq!(e.property_name, Some("MaxClockDrift".to_string()));
367-
assert_eq!(e.property_value, None);
365+
matches!(e.kind(), HLCErrorKind::ClockDrift);
368366
}
369367
}
370368
}
@@ -378,11 +376,7 @@ mod tests {
378376
match hlc.validate(now_ms_precision(), DEFAULT_MAX_CLOCK_DRIFT) {
379377
Ok(()) => panic!("Expected error"),
380378
Err(e) => {
381-
assert_eq!(e.kind, AIOProtocolErrorKind::InternalLogicError);
382-
assert!(e.is_shallow);
383-
assert!(!e.is_remote);
384-
assert_eq!(e.property_name, Some("Counter".to_string()));
385-
assert_eq!(e.property_value, None);
379+
matches!(e.kind(), HLCErrorKind::OverflowWarning);
386380
}
387381
}
388382

@@ -428,9 +422,7 @@ mod tests {
428422
}
429423
Err(e) => {
430424
assert!(!should_succeed);
431-
assert_eq!(e.kind, AIOProtocolErrorKind::StateInvalid);
432-
assert_eq!(e.property_name, Some("MaxClockDrift".to_string()));
433-
assert_eq!(e.property_value, None);
425+
matches!(e.kind(), HLCErrorKind::ClockDrift);
434426
}
435427
}
436428
}
@@ -555,9 +547,7 @@ mod tests {
555547
}
556548
Err(e) => {
557549
assert!(!should_succeed);
558-
assert_eq!(e.kind, AIOProtocolErrorKind::StateInvalid);
559-
assert_eq!(e.property_name, Some("MaxClockDrift".to_string()));
560-
assert_eq!(e.property_value, None);
550+
matches!(e.kind(), HLCErrorKind::ClockDrift);
561551
// self hlc should not have been updated
562552
assert_eq!(self_hlc.counter, 0);
563553
assert_eq!(self_hlc.timestamp, self_ts_copy);
@@ -623,8 +613,7 @@ mod tests {
623613
}
624614
Err(e) => {
625615
assert!(!should_succeed);
626-
assert_eq!(e.kind, AIOProtocolErrorKind::InternalLogicError);
627-
assert_eq!(e.property_name, Some("Counter".to_string()));
616+
matches!(e.kind(), HLCErrorKind::OverflowWarning);
628617
// self hlc should not have been updated
629618
assert_eq!(self_hlc.counter, self_counter_copy);
630619
assert_eq!(self_hlc.timestamp, self_ts_copy);

0 commit comments

Comments
 (0)