1use std::sync::{Arc, Weak};
10use std::time::Duration;
11
12use async_trait::async_trait;
13use dashmap::DashMap;
14
15use crate::error::Result;
16use crate::time::{Clock, Timestamp};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RecoveryAction {
21 Set,
23 Remove,
25 Expire,
27}
28
29#[derive(Debug, Clone)]
31pub struct RecoveryItem {
32 pub key: Arc<str>,
34 pub action: RecoveryAction,
36 pub timestamp: Timestamp,
38 pub expires_at: Timestamp,
40 pub remaining_retries: Option<u32>,
42}
43
44#[derive(Debug, Clone)]
46pub struct RecoveryConfig {
47 pub enabled: bool,
49 pub delay: Duration,
52 pub max_items: Option<usize>,
54 pub max_retries: Option<u32>,
56}
57
58impl Default for RecoveryConfig {
59 fn default() -> Self {
60 Self {
61 enabled: true,
62 delay: Duration::from_secs(2),
63 max_items: None,
64 max_retries: None,
65 }
66 }
67}
68
69#[async_trait]
72pub trait RecoveryExecutor: Send + Sync {
73 async fn replay(&self, item: &RecoveryItem) -> Result<()>;
78}
79
80pub struct AutoRecoveryService {
82 config: RecoveryConfig,
83 clock: Arc<dyn Clock>,
84 queue: DashMap<Arc<str>, RecoveryItem>,
85 executor: std::sync::OnceLock<Weak<dyn RecoveryExecutor>>,
86}
87
88impl AutoRecoveryService {
89 #[must_use]
92 pub fn new(config: RecoveryConfig, clock: Arc<dyn Clock>) -> Arc<Self> {
93 Arc::new(Self {
94 config,
95 clock,
96 queue: DashMap::new(),
97 executor: std::sync::OnceLock::new(),
98 })
99 }
100
101 pub fn set_executor(&self, executor: Weak<dyn RecoveryExecutor>) {
104 let _ = self.executor.set(executor);
105 }
106
107 #[must_use]
109 pub fn len(&self) -> usize {
110 self.queue.len()
111 }
112
113 #[must_use]
115 pub fn is_empty(&self) -> bool {
116 self.queue.is_empty()
117 }
118
119 pub fn enqueue(&self, item: RecoveryItem) {
123 if !self.config.enabled {
124 return;
125 }
126 match self.queue.get(&item.key) {
128 Some(existing) if existing.timestamp >= item.timestamp => return,
129 _ => {}
130 }
131 if let Some(max) = self.config.max_items
132 && self.queue.len() >= max
133 && !self.queue.contains_key(&item.key)
134 {
135 self.evict_one_before(item.expires_at);
136 if self.queue.len() >= max {
137 return; }
139 }
140 let mut item = item;
141 if item.remaining_retries.is_none() {
142 item.remaining_retries = self.config.max_retries;
143 }
144 self.queue.insert(Arc::clone(&item.key), item);
145 }
146
147 fn evict_one_before(&self, bound: Timestamp) {
149 let victim = self
150 .queue
151 .iter()
152 .min_by_key(|e| e.value().expires_at.ticks())
153 .map(|e| (e.key().clone(), e.value().expires_at));
154 if let Some((key, expires_at)) = victim
155 && expires_at < bound
156 {
157 self.queue.remove(&key);
158 }
159 }
160
161 pub fn spawn(self: &Arc<Self>) {
164 if !self.config.enabled {
165 return;
166 }
167 let service = Arc::clone(self);
168 let delay = self.config.delay;
169 tokio::spawn(async move {
170 let mut ticker = tokio::time::interval(delay.max(Duration::from_millis(50)));
171 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
172 loop {
173 ticker.tick().await;
174 let Some(executor) = service.executor.get().and_then(Weak::upgrade) else {
176 if service.executor.get().is_some() {
179 break;
180 }
181 continue;
182 };
183 service.drain_once(executor.as_ref()).await;
184 }
185 });
186 }
187
188 pub async fn drain_once(&self, executor: &dyn RecoveryExecutor) {
190 let now = self.clock.now();
191 let keys: Vec<Arc<str>> = self.queue.iter().map(|e| e.key().clone()).collect();
192 for key in keys {
193 let Some(item) = self.queue.get(&key).map(|e| e.value().clone()) else {
194 continue;
195 };
196 if now >= item.expires_at {
197 self.queue.remove(&key);
198 continue;
199 }
200 match executor.replay(&item).await {
201 Ok(()) => {
202 self.queue.remove(&key);
203 }
204 Err(_) => self.record_failure(&key),
205 }
206 }
207 }
208
209 fn record_failure(&self, key: &Arc<str>) {
211 let mut drop_it = false;
212 if let Some(mut entry) = self.queue.get_mut(key) {
213 match entry.remaining_retries {
214 Some(0) => drop_it = true,
215 Some(n) => entry.remaining_retries = Some(n - 1),
216 None => {}
217 }
218 }
219 if drop_it {
220 self.queue.remove(key);
221 }
222 }
223}