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
98impl<E> core::fmt::Display for ActionPollError<E> {
99 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100 formatter.write_str(match self {
101 Self::InvalidProgress => "action progress exceeds 100",
102 Self::ProgressRegressed => "action progress moved backwards",
103 Self::ZeroDelay => "action poll policy requested a zero delay",
104 Self::ObservationOverflow => "action observation counter overflowed",
105 Self::Terminal => "action polling already reached a terminal state",
106 Self::Policy(_) => "action poll policy failed",
107 })
108 }
109}
110
111impl<E: core::fmt::Debug> core::error::Error for ActionPollError<E> {}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub struct ActionPoller {
116 observations: u32,
117 last_progress: Option<u8>,
118 terminal: bool,
119}
120
121impl ActionPoller {
122 #[must_use]
124 pub const fn new() -> Self {
125 Self {
126 observations: 0,
127 last_progress: None,
128 terminal: false,
129 }
130 }
131
132 #[must_use]
134 pub const fn observations(self) -> u32 {
135 self.observations
136 }
137
138 #[must_use]
140 pub const fn is_terminal(self) -> bool {
141 self.terminal
142 }
143
144 pub fn observe<E, P>(
147 &mut self,
148 update: ActionUpdate<E>,
149 progress: u8,
150 rate_limit: Option<RateLimit>,
151 policy: &mut P,
152 ) -> Result<ActionPollStep<E>, ActionPollError<P::Error>>
153 where
154 P: PollPolicy,
155 {
156 if self.terminal {
157 return Err(ActionPollError::Terminal);
158 }
159 let observations = self
160 .observations
161 .checked_add(1)
162 .ok_or(ActionPollError::ObservationOverflow)?;
163
164 let step = match update {
165 ActionUpdate::Success => ActionPollStep::Complete,
166 ActionUpdate::Failed(error) => ActionPollStep::Failed(error),
167 ActionUpdate::Running => {
168 if progress > 100 {
169 return Err(ActionPollError::InvalidProgress);
170 }
171 if self.last_progress.is_some_and(|last| progress < last) {
172 return Err(ActionPollError::ProgressRegressed);
173 }
174 let context = PollContext {
175 observation: observations,
176 progress,
177 rate_limit,
178 };
179 match policy.decide(context).map_err(ActionPollError::Policy)? {
180 PollDecision::Delay(delay) if delay.is_zero() => {
181 return Err(ActionPollError::ZeroDelay);
182 }
183 PollDecision::Delay(delay) => ActionPollStep::Delay(delay),
184 PollDecision::Cancel => ActionPollStep::Cancelled,
185 PollDecision::Timeout => ActionPollStep::TimedOut,
186 }
187 }
188 };
189
190 self.observations = observations;
191 self.last_progress = Some(progress);
192 self.terminal = !matches!(step, ActionPollStep::Delay(_));
193 Ok(step)
194 }
195}
196
197impl Default for ActionPoller {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203#[cfg(test)]
204mod tests;