1use crate::warm_start::ConfiguredWarmStartStore;
7use crate::warm_start::key::Fingerprint;
8use crate::warm_start::store::{EntryKind, WarmStartEntry, WarmStartStore};
9use std::sync::Mutex;
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12const MIN_CHECKPOINT_INTERVAL: Duration = Duration::from_secs(2);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LoadSource {
20 Exact,
21 Preloaded,
22}
23
24#[derive(Debug, Clone)]
25pub struct LoadedEntry {
26 pub entry: WarmStartEntry,
27 pub source: LoadSource,
28}
29
30#[derive(Debug)]
31pub struct Session {
32 store: WarmStartStore,
33 configured_store: Option<ConfiguredWarmStartStore>,
34 key: Fingerprint,
35 run_id: String,
36 inner: Mutex<Inner>,
37 preloaded: Mutex<Option<WarmStartEntry>>,
45}
46
47#[derive(Debug)]
48struct Inner {
49 last_write: Option<Instant>,
50 best_seen: Option<f64>,
51}
52
53impl Session {
54 pub fn open(store: WarmStartStore, key: Fingerprint) -> Self {
55 let nanos = SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .map(|d| d.as_nanos())
58 .unwrap_or(0);
59 let pid = std::process::id();
60 let run_id = format!("ckpt-r{pid:x}-{nanos:x}");
61 Self {
62 store,
63 configured_store: None,
64 key,
65 run_id,
66 inner: Mutex::new(Inner {
67 last_write: None,
68 best_seen: None,
69 }),
70 preloaded: Mutex::new(None),
71 }
72 }
73
74 pub(super) fn open_configured(
75 store: WarmStartStore,
76 key: Fingerprint,
77 configured_store: ConfiguredWarmStartStore,
78 ) -> Self {
79 let mut session = Self::open(store, key);
80 session.configured_store = Some(configured_store);
81 session
82 }
83
84 fn configured_store_is_available(&self) -> bool {
85 self.configured_store
86 .as_ref()
87 .is_none_or(ConfiguredWarmStartStore::is_available)
88 }
89
90 fn record_store_error(&self, operation: &str, error: &crate::warm_start::StoreError) {
91 if let Some(configured_store) = &self.configured_store {
92 configured_store.record_store_error(operation, error);
93 }
94 }
95
96 pub fn preload(&self, entry: WarmStartEntry) {
105 let mut slot = match self.preloaded.lock() {
106 Ok(g) => g,
107 Err(p) => p.into_inner(),
108 };
109 *slot = Some(entry);
110 }
111
112 pub fn key(&self) -> &Fingerprint {
113 &self.key
114 }
115
116 pub fn run_id(&self) -> &str {
117 &self.run_id
118 }
119
120 pub fn try_load(&self) -> Option<WarmStartEntry> {
130 self.try_load_with_source().map(|loaded| loaded.entry)
131 }
132
133 pub fn try_load_with_source(&self) -> Option<LoadedEntry> {
140 if let Ok(mut slot) = self.preloaded.lock()
141 && let Some(entry) = slot.take()
142 {
143 return Some(LoadedEntry {
144 entry,
145 source: LoadSource::Preloaded,
146 });
147 }
148 if !self.configured_store_is_available() {
149 return None;
150 }
151 match self.store.lookup(&self.key) {
152 Ok(Some(entry)) => Some(LoadedEntry {
153 entry,
154 source: LoadSource::Exact,
155 }),
156 Ok(None) => None,
157 Err(error) => {
158 self.record_store_error("load outer-iterate session", &error);
159 None
160 }
161 }
162 }
163
164
165 pub fn peek_load_with_source(&self) -> Option<LoadedEntry> {
168 if let Ok(slot) = self.preloaded.lock()
169 && let Some(entry) = slot.as_ref()
170 {
171 return Some(LoadedEntry {
172 entry: entry.clone(),
173 source: LoadSource::Preloaded,
174 });
175 }
176 if !self.configured_store_is_available() {
177 return None;
178 }
179 match self.store.lookup(&self.key) {
180 Ok(Some(entry)) => Some(LoadedEntry {
181 entry,
182 source: LoadSource::Exact,
183 }),
184 Ok(None) => None,
185 Err(error) => {
186 self.record_store_error("peek outer-iterate session", &error);
187 None
188 }
189 }
190 }
191
192 pub fn checkpoint(
196 &self,
197 payload: &[u8],
198 objective: Option<f64>,
199 iteration: Option<u64>,
200 ) -> bool {
201 if !self.configured_store_is_available() {
202 return false;
203 }
204 let now = Instant::now();
205 let mut guard = match self.inner.lock() {
206 Ok(g) => g,
207 Err(p) => p.into_inner(),
208 };
209 let improves = match (objective, guard.best_seen) {
210 (Some(o), Some(b)) => o < b - 1e-12,
211 (Some(_), None) => true,
212 _ => false,
213 };
214 if !improves
215 && let Some(last) = guard.last_write
216 && now.duration_since(last) < MIN_CHECKPOINT_INTERVAL
217 {
218 return false;
219 }
220 match self.store.save_overwrite(
221 &self.key,
222 &self.run_id,
223 payload,
224 objective,
225 iteration,
226 EntryKind::Checkpoint,
227 ) {
228 Ok(()) => {
229 guard.last_write = Some(now);
230 if let Some(o) = objective {
231 guard.best_seen = Some(match guard.best_seen {
232 Some(b) => b.min(o),
233 None => o,
234 });
235 }
236 true
237 }
238 Err(error) => {
239 self.record_store_error("checkpoint outer-iterate session", &error);
240 false
241 }
242 }
243 }
244
245 pub fn finalize(&self, payload: &[u8], objective: Option<f64>, iteration: Option<u64>) -> bool {
248 if !self.configured_store_is_available() {
249 return false;
250 }
251 match self.store.save_overwrite(
252 &self.key,
253 &self.run_id,
254 payload,
255 objective,
256 iteration,
257 EntryKind::Final,
258 ) {
259 Ok(()) => true,
260 Err(error) => {
261 self.record_store_error("finalize outer-iterate session", &error);
262 false
263 }
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::warm_start::key::Fingerprinter;
272 use crate::warm_start::store::StoreOptions;
273
274 fn temp_session(label: &str) -> (tempfile::TempDir, Session) {
275 let dir = tempfile::tempdir().unwrap();
276 let store = WarmStartStore::open(
277 dir.path().to_path_buf(),
278 StoreOptions {
279 size_budget_bytes: 1024 * 1024,
280 ttl: Duration::from_secs(60),
281 },
282 )
283 .unwrap();
284 let mut fp = Fingerprinter::new();
285 fp.absorb_str(b"label", label);
286 let key = fp.finalize();
287 let s = Session::open(store, key);
288 (dir, s)
289 }
290
291 #[test]
292 fn checkpoint_then_load() {
293 let (_d, s) = temp_session("ckpt");
294 assert!(s.checkpoint(b"iter-1", Some(2.0), Some(1)));
295 let got = s.try_load().unwrap();
296 assert_eq!(got.payload, b"iter-1");
297 assert_eq!(got.objective, Some(2.0));
298 assert_eq!(got.kind, EntryKind::Checkpoint);
299 }
300
301 #[test]
302 fn improving_objective_bypasses_rate_limit() {
303 let (_d, s) = temp_session("improve");
304 assert!(s.checkpoint(b"a", Some(5.0), Some(1)));
305 assert!(s.checkpoint(b"b", Some(3.0), Some(2)));
308 let got = s.try_load().unwrap();
309 assert_eq!(got.payload, b"b");
310 assert_eq!(got.objective, Some(3.0));
311 }
312
313 #[test]
314 fn non_improving_writes_are_throttled() {
315 let (_d, s) = temp_session("throttle");
316 assert!(s.checkpoint(b"a", Some(2.0), Some(1)));
317 assert!(!s.checkpoint(b"b", Some(5.0), Some(2)));
319 let got = s.try_load().unwrap();
321 assert_eq!(got.payload, b"a");
322 }
323
324 #[test]
325 fn finalize_promotes_to_final_kind() {
326 let (_d, s) = temp_session("final");
327 s.checkpoint(b"ckpt", Some(2.0), Some(1));
328 s.finalize(b"done", Some(1.0), Some(5));
329 let got = s.try_load().unwrap();
330 assert_eq!(got.payload, b"done");
331 assert_eq!(got.kind, EntryKind::Final);
332 }
333
334 #[test]
335 fn preload_takes_precedence_over_store_lookup() {
336 let (_d, s) = temp_session("preload-empty");
340 assert!(s.try_load().is_none(), "fresh key should have no entry");
341
342 let seeded = WarmStartEntry {
343 payload: b"from-prefix".to_vec(),
344 objective: Some(7.0),
345 iteration: Some(42),
346 kind: EntryKind::Final,
347 written_unix_secs: 0,
348 };
349 s.preload(seeded);
350
351 let got = s.try_load().expect("preloaded seed should be returned");
352 assert_eq!(got.payload, b"from-prefix");
353 assert_eq!(got.objective, Some(7.0));
354 }
355
356 #[test]
357 fn preload_consumed_on_first_try_load() {
358 let (_d, s) = temp_session("preload-consume");
363 s.checkpoint(b"exact", Some(2.0), Some(5));
364
365 let seeded = WarmStartEntry {
366 payload: b"seed".to_vec(),
367 objective: Some(99.0),
368 iteration: Some(1),
369 kind: EntryKind::Checkpoint,
370 written_unix_secs: 0,
371 };
372 s.preload(seeded);
373
374 let first = s.try_load().expect("first call should return seed");
376 assert_eq!(first.payload, b"seed");
377
378 let second = s.try_load().expect("second call should read from store");
380 assert_eq!(second.payload, b"exact");
381 }
382
383 #[test]
384 fn peek_load_does_not_consume_preloaded_seed() {
385 let (_d, s) = temp_session("preload-peek");
386 let seeded = WarmStartEntry {
387 payload: b"seed".to_vec(),
388 objective: Some(3.0),
389 iteration: Some(9),
390 kind: EntryKind::Final,
391 written_unix_secs: 0,
392 };
393 s.preload(seeded);
394
395 let peeked = s
396 .peek_load_with_source()
397 .expect("peek should see preloaded seed");
398 assert_eq!(peeked.entry.payload, b"seed");
399 assert_eq!(peeked.source, LoadSource::Preloaded);
400
401 let loaded = s
402 .try_load()
403 .expect("try_load should still receive the preloaded seed");
404 assert_eq!(loaded.payload, b"seed");
405 assert!(
406 s.try_load().is_none(),
407 "preloaded seed should be consumed only by try_load"
408 );
409 }
410
411 #[test]
412 fn second_session_reads_first_session_checkpoint() {
413 let dir = tempfile::tempdir().unwrap();
414 let mut fp = Fingerprinter::new();
415 fp.absorb_str(b"k", "shared");
416 let key = fp.finalize();
417
418 let store_a = WarmStartStore::open(
419 dir.path().to_path_buf(),
420 StoreOptions {
421 size_budget_bytes: 1024 * 1024,
422 ttl: Duration::from_secs(60),
423 },
424 )
425 .unwrap();
426 let s_a = Session::open(store_a, key);
427 s_a.checkpoint(b"from-a", Some(1.0), Some(3));
428
429 let store_b = WarmStartStore::open(
431 dir.path().to_path_buf(),
432 StoreOptions {
433 size_budget_bytes: 1024 * 1024,
434 ttl: Duration::from_secs(60),
435 },
436 )
437 .unwrap();
438 let s_b = Session::open(store_b, key);
439 let got = s_b.try_load().unwrap();
440 assert_eq!(got.payload, b"from-a");
441 assert_eq!(got.objective, Some(1.0));
442 }
443}