cloud_sdk/
action_polling.rs1use core::time::Duration;
4
5use crate::rate_limit::RateLimit;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ActionUpdate<E> {
10 Running,
12 Success,
14 Failed(E),
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct PollContext {
21 observation: u32,
22 progress: u8,
23 rate_limit: Option<RateLimit>,
24}
25
26impl PollContext {
27 #[must_use]
29 pub const fn observation(self) -> u32 {
30 self.observation
31 }
32
33 #[must_use]
35 pub const fn progress(self) -> u8 {
36 self.progress
37 }
38
39 #[must_use]
41 pub const fn rate_limit(self) -> Option<RateLimit> {
42 self.rate_limit
43 }
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum PollDecision {
49 Delay(Duration),
51 Cancel,
53 Timeout,
55}
56
57pub trait PollPolicy {
59 type Error;
61
62 fn decide(&mut self, context: PollContext) -> Result<PollDecision, Self::Error>;
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum ActionPollStep<E> {
69 Delay(Duration),
71 Complete,
73 Failed(E),
75 Cancelled,
77 TimedOut,
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum ActionPollError<E> {
84 InvalidProgress,
86 ProgressRegressed,
88 ZeroDelay,
90 ObservationOverflow,
92 Terminal,
94 Policy(E),
96}
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub struct ActionPoller {
101 observations: u32,
102 last_progress: Option<u8>,
103 terminal: bool,
104}
105
106impl ActionPoller {
107 #[must_use]
109 pub const fn new() -> Self {
110 Self {
111 observations: 0,
112 last_progress: None,
113 terminal: false,
114 }
115 }
116
117 #[must_use]
119 pub const fn observations(self) -> u32 {
120 self.observations
121 }
122
123 #[must_use]
125 pub const fn is_terminal(self) -> bool {
126 self.terminal
127 }
128
129 pub fn observe<E, P>(
132 &mut self,
133 update: ActionUpdate<E>,
134 progress: u8,
135 rate_limit: Option<RateLimit>,
136 policy: &mut P,
137 ) -> Result<ActionPollStep<E>, ActionPollError<P::Error>>
138 where
139 P: PollPolicy,
140 {
141 if self.terminal {
142 return Err(ActionPollError::Terminal);
143 }
144 let observations = self
145 .observations
146 .checked_add(1)
147 .ok_or(ActionPollError::ObservationOverflow)?;
148
149 let step = match update {
150 ActionUpdate::Success => ActionPollStep::Complete,
151 ActionUpdate::Failed(error) => ActionPollStep::Failed(error),
152 ActionUpdate::Running => {
153 if progress > 100 {
154 return Err(ActionPollError::InvalidProgress);
155 }
156 if self.last_progress.is_some_and(|last| progress < last) {
157 return Err(ActionPollError::ProgressRegressed);
158 }
159 let context = PollContext {
160 observation: observations,
161 progress,
162 rate_limit,
163 };
164 match policy.decide(context).map_err(ActionPollError::Policy)? {
165 PollDecision::Delay(delay) if delay.is_zero() => {
166 return Err(ActionPollError::ZeroDelay);
167 }
168 PollDecision::Delay(delay) => ActionPollStep::Delay(delay),
169 PollDecision::Cancel => ActionPollStep::Cancelled,
170 PollDecision::Timeout => ActionPollStep::TimedOut,
171 }
172 }
173 };
174
175 self.observations = observations;
176 self.last_progress = Some(progress);
177 self.terminal = !matches!(step, ActionPollStep::Delay(_));
178 Ok(step)
179 }
180}
181
182impl Default for ActionPoller {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188#[cfg(test)]
189mod tests;