Skip to main content

lodestone_client/
operation_waiter.rs

1use lodestone_wire::{
2    LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS, LODESTONE_API_MAX_OPERATION_WAIT_INTERVAL_MILLIS, Locator, Operation,
3    OperationState, U64, WaitOperationRequest,
4};
5
6use crate::{CallOptions, Client, ClientError};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct OperationWaitOptions {
10    pub maximum_attempts: u32,
11    pub interval_millis: u64,
12}
13
14impl Default for OperationWaitOptions {
15    fn default() -> Self {
16        Self {
17            maximum_attempts: 20,
18            interval_millis: 100,
19        }
20    }
21}
22
23#[derive(Clone, Debug, PartialEq)]
24pub struct OperationWaitOutcome {
25    pub operation: Operation,
26    pub attempts: u32,
27    pub terminal: bool,
28}
29
30#[derive(Debug)]
31pub enum OperationWaitError {
32    InvalidOptions,
33    InvalidAuthorityRevision,
34    Client(ClientError),
35}
36
37impl Client {
38    /// Performs one server-bounded operation wait under the supplied absolute
39    /// call deadline. It never creates a fresh per-attempt deadline.
40    pub async fn wait_for_operation(
41        &self,
42        locator: Locator,
43        after_authority_revision: U64,
44        wait: OperationWaitOptions,
45        options: CallOptions,
46    ) -> Result<OperationWaitOutcome, OperationWaitError> {
47        validate_wait_options(wait)?;
48        if after_authority_revision.validate().is_err() {
49            return Err(OperationWaitError::InvalidAuthorityRevision);
50        }
51        let result = self
52            .wait_operation(
53                WaitOperationRequest {
54                    locator,
55                    after_authority_revision,
56                    maximum_attempts: wait.maximum_attempts,
57                    interval_millis: U64 {
58                        value: wait.interval_millis.to_string(),
59                    },
60                },
61                options,
62            )
63            .await
64            .map_err(OperationWaitError::Client)?;
65        let terminal = operation_is_terminal(result.operation.current_state);
66        Ok(OperationWaitOutcome {
67            operation: result.operation,
68            attempts: result.attempts,
69            terminal,
70        })
71    }
72}
73
74fn validate_wait_options(options: OperationWaitOptions) -> Result<(), OperationWaitError> {
75    if options.maximum_attempts == 0
76        || options.maximum_attempts > LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS
77        || options.interval_millis == 0
78        || options.interval_millis > LODESTONE_API_MAX_OPERATION_WAIT_INTERVAL_MILLIS
79    {
80        Err(OperationWaitError::InvalidOptions)
81    } else {
82        Ok(())
83    }
84}
85
86const fn operation_is_terminal(state: OperationState) -> bool {
87    !matches!(state, OperationState::Pending)
88}
89
90impl std::fmt::Display for OperationWaitError {
91    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        formatter.write_str(match self {
93            Self::InvalidOptions => "operation wait options exceed the native bounded contract",
94            Self::InvalidAuthorityRevision => "operation wait authority revision must be a canonical u64",
95            Self::Client(_) => "operation wait request failed",
96        })
97    }
98}
99
100impl std::error::Error for OperationWaitError {}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn operation_wait_options_are_bounded_by_native_limits() {
108        assert!(validate_wait_options(OperationWaitOptions::default()).is_ok());
109        assert!(matches!(
110            validate_wait_options(OperationWaitOptions {
111                maximum_attempts: LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS.saturating_add(1),
112                interval_millis: 1,
113            }),
114            Err(OperationWaitError::InvalidOptions)
115        ));
116        assert!(matches!(
117            validate_wait_options(OperationWaitOptions {
118                maximum_attempts: 1,
119                interval_millis: 0,
120            }),
121            Err(OperationWaitError::InvalidOptions)
122        ));
123    }
124
125    #[test]
126    fn operation_wait_accepts_the_wire_zero_revision_and_classifies_terminal_state() {
127        assert!(U64 { value: "0".to_owned() }.validate().is_ok());
128        assert!(!operation_is_terminal(OperationState::Pending));
129        assert!(operation_is_terminal(OperationState::Succeeded));
130    }
131}