templar_curator_primitives/policy/refresh_plan/
mod.rs1use alloc::vec::Vec;
2use core::num::NonZeroU64;
3use templar_vault_kernel::{DurationNs, TargetId, TimestampNs};
4
5use super::cooldown::Cooldown;
6use super::target_set::find_first_duplicate;
7
8#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
9#[derive(Clone)]
10pub struct RefreshPlan {
11 targets: Vec<TargetId>,
12}
13
14#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
15#[derive(Clone, Copy, PartialEq, Eq)]
16pub struct RefreshThrottle {
17 cooldown: Cooldown,
18}
19
20#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
21#[derive(Clone, Copy, PartialEq, Eq)]
22pub struct RefreshTargetStatus {
23 target_id: TargetId,
24 last_refresh_at: Option<TimestampNs>,
25}
26
27#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
28#[derive(Clone, Copy, PartialEq, Eq)]
29pub struct RefreshTiming {
30 cooldown: DurationNs,
31 last_refresh_at: Option<TimestampNs>,
32}
33
34#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
35#[derive(Clone)]
36pub struct RefreshExecutionPlan {
37 plan: RefreshPlan,
38 throttle: RefreshThrottle,
39}
40
41impl RefreshPlan {
42 pub fn new(targets: Vec<TargetId>) -> Result<Self, RefreshPlanError> {
43 if targets.is_empty() {
44 return Err(RefreshPlanError::EmptyPlan);
45 }
46
47 if let Some(dup) = find_first_duplicate(&targets) {
48 return Err(RefreshPlanError::DuplicateTarget { target_id: dup });
49 }
50
51 Ok(Self { targets })
52 }
53
54 #[must_use]
55 pub fn len(&self) -> usize {
56 self.targets.len()
57 }
58
59 #[must_use]
60 pub fn is_empty(&self) -> bool {
61 self.targets.is_empty()
62 }
63
64 #[must_use]
65 pub fn targets(&self) -> &[TargetId] {
66 &self.targets
67 }
68
69 #[must_use]
70 pub fn into_targets(self) -> Vec<TargetId> {
71 self.targets
72 }
73}
74
75impl RefreshThrottle {
76 #[must_use]
77 pub fn default_unlimited() -> Self {
78 Self {
79 cooldown: Cooldown::unlimited(),
80 }
81 }
82
83 #[must_use]
84 pub fn new(cooldown: DurationNs, last_refresh_at: Option<TimestampNs>) -> Self {
85 let cooldown = NonZeroU64::new(cooldown.as_u64())
86 .map_or_else(Cooldown::unlimited, Cooldown::new)
87 .with_last_event_ns(last_refresh_at.map(TimestampNs::as_u64));
88
89 Self { cooldown }
90 }
91
92 #[must_use]
93 pub fn cooldown(&self) -> &Cooldown {
94 &self.cooldown
95 }
96
97 #[must_use]
98 pub fn is_ready(&self, current_time: TimestampNs) -> bool {
99 self.cooldown.is_ready(current_time.as_u64())
100 }
101
102 pub fn check(&self, current_time: TimestampNs) -> Result<(), RefreshPlanError> {
103 self.cooldown
104 .check(current_time.as_u64())
105 .map_err(|e| match e {
106 super::cooldown::CooldownError::OnCooldown {
107 ready_at_ns,
108 remaining_ns,
109 } => RefreshPlanError::OnCooldown {
110 ready_at: TimestampNs(ready_at_ns),
111 remaining: DurationNs(remaining_ns),
112 },
113 })
114 }
115
116 pub fn try_acquire(self, current_time: TimestampNs) -> Result<Self, RefreshPlanError> {
117 self.cooldown
118 .try_acquire(current_time.as_u64())
119 .map(|cooldown| Self { cooldown })
120 .map_err(|e| match e {
121 super::cooldown::CooldownError::OnCooldown {
122 ready_at_ns,
123 remaining_ns,
124 } => RefreshPlanError::OnCooldown {
125 ready_at: TimestampNs(ready_at_ns),
126 remaining: DurationNs(remaining_ns),
127 },
128 })
129 }
130
131 #[must_use]
132 pub fn record_completion(mut self, completed_at: TimestampNs) -> Self {
133 self.cooldown = self.cooldown.recorded_at(completed_at.as_u64());
134 self
135 }
136
137 #[must_use]
138 pub fn cooldown_duration(&self) -> DurationNs {
139 DurationNs(self.cooldown.interval_ns().map_or(0, NonZeroU64::get))
140 }
141
142 #[must_use]
143 pub fn last_refresh_at(&self) -> Option<TimestampNs> {
144 self.cooldown.last_event_ns().map(TimestampNs)
145 }
146
147 #[must_use]
148 pub fn cooldown_ns(&self) -> u64 {
149 self.cooldown_duration().as_u64()
150 }
151
152 #[must_use]
153 pub fn last_refresh_ns(&self) -> Option<u64> {
154 self.last_refresh_at().map(TimestampNs::as_u64)
155 }
156}
157
158impl Default for RefreshThrottle {
159 fn default() -> Self {
160 Self::default_unlimited()
161 }
162}
163
164impl RefreshTargetStatus {
165 #[must_use]
166 pub const fn new(target_id: TargetId, last_refresh_at: Option<TimestampNs>) -> Self {
167 Self {
168 target_id,
169 last_refresh_at,
170 }
171 }
172
173 #[must_use]
174 pub const fn target_id(&self) -> TargetId {
175 self.target_id
176 }
177
178 #[must_use]
179 pub const fn last_refresh_at(&self) -> Option<TimestampNs> {
180 self.last_refresh_at
181 }
182
183 #[must_use]
184 pub const fn last_refresh_ns(&self) -> Option<u64> {
185 match self.last_refresh_at {
186 Some(last_refresh_at) => Some(last_refresh_at.as_u64()),
187 None => None,
188 }
189 }
190}
191
192impl RefreshTiming {
193 #[must_use]
194 pub const fn new(cooldown: DurationNs, last_refresh_at: Option<TimestampNs>) -> Self {
195 Self {
196 cooldown,
197 last_refresh_at,
198 }
199 }
200
201 #[must_use]
202 pub const fn cooldown(&self) -> DurationNs {
203 self.cooldown
204 }
205
206 #[must_use]
207 pub const fn last_refresh_at(&self) -> Option<TimestampNs> {
208 self.last_refresh_at
209 }
210}
211
212impl RefreshExecutionPlan {
213 #[must_use]
214 pub const fn new(plan: RefreshPlan, throttle: RefreshThrottle) -> Self {
215 Self { plan, throttle }
216 }
217
218 #[must_use]
219 pub const fn plan(&self) -> &RefreshPlan {
220 &self.plan
221 }
222
223 #[must_use]
224 pub const fn throttle(&self) -> &RefreshThrottle {
225 &self.throttle
226 }
227
228 #[must_use]
229 pub fn into_parts(self) -> (RefreshPlan, RefreshThrottle) {
230 (self.plan, self.throttle)
231 }
232}
233
234#[templar_vault_macros::vault_derive]
235#[derive(Clone, PartialEq, Eq)]
236pub enum RefreshPlanError {
237 EmptyPlan,
238 OnCooldown {
239 ready_at: TimestampNs,
240 remaining: DurationNs,
241 },
242 DuplicateTarget {
243 target_id: TargetId,
244 },
245 TargetNotFound {
246 target_id: TargetId,
247 },
248 FutureRefreshTimestamp {
249 target_id: TargetId,
250 last_refresh_at: TimestampNs,
251 current_time: TimestampNs,
252 },
253}
254
255pub fn build_refresh_plan(enabled_targets: &[TargetId]) -> Result<RefreshPlan, RefreshPlanError> {
256 RefreshPlan::new(enabled_targets.to_vec())
257}
258
259pub fn build_targeted_refresh_plan(
260 targets: &[TargetId],
261 enabled_targets: &[TargetId],
262) -> Result<RefreshPlan, RefreshPlanError> {
263 let plan = RefreshPlan::new(targets.to_vec())?;
264
265 for target in plan.targets() {
266 if !enabled_targets.contains(target) {
267 return Err(RefreshPlanError::TargetNotFound { target_id: *target });
268 }
269 }
270
271 Ok(plan)
272}
273
274pub fn refresh_execution_plan(
275 targets: &[TargetId],
276 timing: RefreshTiming,
277) -> Result<RefreshExecutionPlan, RefreshPlanError> {
278 let plan = RefreshPlan::new(targets.to_vec())?;
279 let throttle = RefreshThrottle::new(timing.cooldown(), timing.last_refresh_at());
280 Ok(RefreshExecutionPlan::new(plan, throttle))
281}
282
283pub fn build_stale_refresh_plan(
284 all_targets: &[RefreshTargetStatus],
285 max_age: DurationNs,
286 current_time: TimestampNs,
287 enabled_targets: &[TargetId],
288) -> Result<Option<RefreshPlan>, RefreshPlanError> {
289 let stale_targets = filter_stale_targets(all_targets, max_age, current_time)?;
290 if stale_targets.is_empty() {
291 return Ok(None);
292 }
293
294 build_targeted_refresh_plan(&stale_targets, enabled_targets).map(Some)
295}
296
297pub fn filter_stale_targets(
298 all_targets: &[RefreshTargetStatus],
299 max_age: DurationNs,
300 current_time: TimestampNs,
301) -> Result<Vec<TargetId>, RefreshPlanError> {
302 let mut stale_targets = Vec::new();
303
304 for target in all_targets {
305 match target.last_refresh_at() {
306 None => stale_targets.push(target.target_id()),
307 Some(last_refresh_at) if last_refresh_at > current_time => {
308 return Err(RefreshPlanError::FutureRefreshTimestamp {
309 target_id: target.target_id(),
310 last_refresh_at,
311 current_time,
312 });
313 }
314 Some(last_refresh_at)
315 if current_time
316 .as_u64()
317 .saturating_sub(last_refresh_at.as_u64())
318 > max_age.as_u64() =>
319 {
320 stale_targets.push(target.target_id());
321 }
322 Some(_) => {}
323 }
324 }
325
326 Ok(stale_targets)
327}