Skip to main content

scheduler/store/
resilient.rs

1use super::{InMemoryStateStore, ResilientStoreError, StateStore, StoreEvent, StoreOperation};
2use crate::error::StoreErrorKind;
3use crate::model::JobState;
4use std::collections::{HashMap, VecDeque};
5use std::sync::atomic::{AtomicBool, Ordering};
6use tokio::sync::Mutex;
7
8/// Wraps a primary store with an in-process mirror that takes over after
9/// connection-class failures and opportunistically writes dirty state back once
10/// the primary accepts commands again.
11#[derive(Debug)]
12pub struct ResilientStateStore<S>
13where
14    S: StateStore,
15    S::Error: ResilientStoreError,
16{
17    primary: S,
18    degraded: AtomicBool,
19    mirror: InMemoryStateStore,
20    dirty: Mutex<HashMap<String, Option<JobState>>>,
21    events: Mutex<VecDeque<StoreEvent>>,
22}
23
24impl<S> ResilientStateStore<S>
25where
26    S: StateStore,
27    S::Error: ResilientStoreError,
28{
29    pub fn new(store: S) -> Self {
30        Self {
31            primary: store,
32            degraded: AtomicBool::new(false),
33            mirror: InMemoryStateStore::new(),
34            dirty: Mutex::new(HashMap::new()),
35            events: Mutex::new(VecDeque::new()),
36        }
37    }
38
39    pub fn degraded(store: S) -> Self {
40        Self {
41            primary: store,
42            degraded: AtomicBool::new(true),
43            mirror: InMemoryStateStore::new(),
44            dirty: Mutex::new(HashMap::new()),
45            events: Mutex::new(VecDeque::new()),
46        }
47    }
48
49    pub fn from_result(result: Result<S, S::Error>) -> Result<Self, S::Error> {
50        match result {
51            Ok(store) => Ok(Self::new(store)),
52            Err(error) if error.is_connection_issue() => Err(error),
53            Err(error) => Err(error),
54        }
55    }
56
57    /// Returns true once the store has permanently fallen back to its
58    /// in-process mirror.
59    pub fn is_degraded(&self) -> bool {
60        self.degraded.load(Ordering::SeqCst)
61    }
62
63    async fn load_mirror(&self, job_id: &str) -> Result<Option<JobState>, S::Error> {
64        match self.mirror.load(job_id).await {
65            Ok(state) => Ok(state),
66            Err(never) => match never {},
67        }
68    }
69
70    async fn save_mirror(&self, state: &JobState) -> Result<(), S::Error> {
71        match self.mirror.save(state).await {
72            Ok(()) => Ok(()),
73            Err(never) => match never {},
74        }
75    }
76
77    async fn delete_mirror(&self, job_id: &str) -> Result<(), S::Error> {
78        match self.mirror.delete(job_id).await {
79            Ok(()) => Ok(()),
80            Err(never) => match never {},
81        }
82    }
83
84    async fn sync_mirror(&self, job_id: &str, state: Option<&JobState>) -> Result<(), S::Error> {
85        match state {
86            Some(state) => self.save_mirror(state).await,
87            None => self.delete_mirror(job_id).await,
88        }
89    }
90
91    async fn mark_degraded(&self, operation: StoreOperation, error: &S::Error) {
92        let was_degraded = self.degraded.swap(true, Ordering::SeqCst);
93        if !was_degraded {
94            self.events.lock().await.push_back(StoreEvent::Degraded {
95                operation,
96                error: error.to_string(),
97            });
98        }
99    }
100
101    async fn mark_recovering(&self, operation: StoreOperation) {
102        self.events
103            .lock()
104            .await
105            .push_back(StoreEvent::Recovering { operation });
106    }
107
108    async fn mark_recovered(&self, operation: StoreOperation) {
109        let was_degraded = self.degraded.swap(false, Ordering::SeqCst);
110        if was_degraded {
111            self.events
112                .lock()
113                .await
114                .push_back(StoreEvent::Recovered { operation });
115        }
116    }
117
118    async fn mark_recovery_failed(&self, operation: StoreOperation, error: &S::Error) {
119        self.events
120            .lock()
121            .await
122            .push_back(StoreEvent::RecoveryFailed {
123                operation,
124                error: error.to_string(),
125            });
126    }
127
128    async fn record_dirty(&self, job_id: String, state: Option<JobState>) {
129        self.dirty.lock().await.insert(job_id, state);
130    }
131
132    async fn clear_dirty(&self, job_id: &str, operation: StoreOperation) {
133        let mut dirty = self.dirty.lock().await;
134        dirty.remove(job_id);
135        if dirty.is_empty() {
136            drop(dirty);
137            self.mark_recovered(operation).await;
138        }
139    }
140
141    async fn dirty_state(&self, job_id: &str) -> Option<Option<JobState>> {
142        self.dirty.lock().await.get(job_id).cloned()
143    }
144}
145
146impl<S> StateStore for ResilientStateStore<S>
147where
148    S: StateStore + Send + Sync,
149    S::Error: ResilientStoreError,
150{
151    type Error = S::Error;
152
153    async fn load(&self, job_id: &str) -> Result<Option<JobState>, Self::Error> {
154        if self.is_degraded() {
155            if let Some(dirty) = self.dirty_state(job_id).await {
156                return Ok(dirty);
157            }
158        }
159
160        match self.primary.load(job_id).await {
161            Ok(state) => {
162                self.sync_mirror(job_id, state.as_ref()).await?;
163                if self.is_degraded() {
164                    self.mark_recovered(StoreOperation::Load).await;
165                }
166                Ok(state)
167            }
168            Err(error) if error.is_connection_issue() => {
169                self.mark_degraded(StoreOperation::Load, &error).await;
170                self.load_mirror(job_id).await
171            }
172            Err(error) => Err(error),
173        }
174    }
175
176    async fn save(&self, state: &JobState) -> Result<(), Self::Error> {
177        self.save_mirror(state).await?;
178        if self.is_degraded() {
179            self.record_dirty(state.job_id.clone(), Some(state.clone()))
180                .await;
181            self.mark_recovering(StoreOperation::Save).await;
182        }
183
184        match self.primary.save(state).await {
185            Ok(()) => {
186                self.clear_dirty(&state.job_id, StoreOperation::Save).await;
187                Ok(())
188            }
189            Err(error) if error.is_connection_issue() => {
190                self.mark_degraded(StoreOperation::Save, &error).await;
191                self.record_dirty(state.job_id.clone(), Some(state.clone()))
192                    .await;
193                if self.is_degraded() {
194                    self.mark_recovery_failed(StoreOperation::Save, &error)
195                        .await;
196                }
197                Ok(())
198            }
199            Err(error) => Err(error),
200        }
201    }
202
203    async fn delete(&self, job_id: &str) -> Result<(), Self::Error> {
204        self.delete_mirror(job_id).await?;
205        if self.is_degraded() {
206            self.record_dirty(job_id.to_string(), None).await;
207            self.mark_recovering(StoreOperation::Delete).await;
208        }
209
210        match self.primary.delete(job_id).await {
211            Ok(()) => {
212                self.clear_dirty(job_id, StoreOperation::Delete).await;
213                Ok(())
214            }
215            Err(error) if error.is_connection_issue() => {
216                self.mark_degraded(StoreOperation::Delete, &error).await;
217                self.record_dirty(job_id.to_string(), None).await;
218                if self.is_degraded() {
219                    self.mark_recovery_failed(StoreOperation::Delete, &error)
220                        .await;
221                }
222                Ok(())
223            }
224            Err(error) => Err(error),
225        }
226    }
227
228    async fn drain_events(&self) -> Result<Vec<StoreEvent>, Self::Error> {
229        let mut events = self.events.lock().await;
230        Ok(events.drain(..).collect())
231    }
232
233    fn classify_error(error: &Self::Error) -> StoreErrorKind
234    where
235        Self: Sized,
236    {
237        S::classify_error(error)
238    }
239}