Skip to main content

gam_runtime/warm_start/
session.rs

1//! A `Session` ties a `WarmStartStore` to a specific `Fingerprint` so callers
2//! can resume + checkpoint a single fit without re-passing the key on every
3//! call. One session corresponds to one in-flight fit; periodic checkpoints
4//! overwrite a single run-id slot so we don't accumulate one entry per write.
5
6use 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
12/// Minimum gap between checkpoint writes. Auto-derived; never less, so a
13/// tight loop can't thrash disk. Improvements over the best-so-far always
14/// bypass the rate limit — losing the best iterate to a hard crash is the
15/// failure mode this whole module exists to prevent.
16const 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    /// Pre-loaded seed payload from a hierarchical near-match key.
38    ///
39    /// Populated by callers who looked up a related (but not exact-match)
40    /// entry from a different key in the same store. The first call to
41    /// [`Self::try_load`] returns and clears this slot — so the session
42    /// can be used as a unified "load best seed, save under exact key"
43    /// abstraction regardless of where the seed came from.
44    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 key(&self) -> &Fingerprint {
97        &self.key
98    }
99
100    /// Read the best available warm-start entry and report whether it came
101    /// from this session's exact key or from a preloaded near-match seed.
102    ///
103    /// Callers that only need a seed can use `Self::try_load`. Callers that
104    /// may skip expensive validation on a finalized exact hit need this source
105    /// bit so a near-match prefix seed is never mistaken for a completed fit.
106    pub fn try_load_with_source(&self) -> Option<LoadedEntry> {
107        if let Ok(mut slot) = self.preloaded.lock()
108            && let Some(entry) = slot.take()
109        {
110            return Some(LoadedEntry {
111                entry,
112                source: LoadSource::Preloaded,
113            });
114        }
115        if !self.configured_store_is_available() {
116            return None;
117        }
118        match self.store.lookup(&self.key) {
119            Ok(Some(entry)) => Some(LoadedEntry {
120                entry,
121                source: LoadSource::Exact,
122            }),
123            Ok(None) => None,
124            Err(error) => {
125                self.record_store_error("load outer-iterate session", &error);
126                None
127            }
128        }
129    }
130
131    /// Read the currently available warm-start entry with source metadata,
132    /// without consuming a preloaded near-match seed.
133    pub fn peek_load_with_source(&self) -> Option<LoadedEntry> {
134        if let Ok(slot) = self.preloaded.lock()
135            && let Some(entry) = slot.as_ref()
136        {
137            return Some(LoadedEntry {
138                entry: entry.clone(),
139                source: LoadSource::Preloaded,
140            });
141        }
142        if !self.configured_store_is_available() {
143            return None;
144        }
145        match self.store.lookup(&self.key) {
146            Ok(Some(entry)) => Some(LoadedEntry {
147                entry,
148                source: LoadSource::Exact,
149            }),
150            Ok(None) => None,
151            Err(error) => {
152                self.record_store_error("peek outer-iterate session", &error);
153                None
154            }
155        }
156    }
157
158    /// Persist a mid-fit checkpoint. Rate-limited; returns true if a write
159    /// actually happened. Always writes when the new objective strictly
160    /// improves on the best-so-far observed in this session.
161    pub fn checkpoint(
162        &self,
163        payload: &[u8],
164        objective: Option<f64>,
165        iteration: Option<u64>,
166    ) -> bool {
167        if !self.configured_store_is_available() {
168            return false;
169        }
170        let now = Instant::now();
171        let mut guard = match self.inner.lock() {
172            Ok(g) => g,
173            Err(p) => p.into_inner(),
174        };
175        let improves = match (objective, guard.best_seen) {
176            (Some(o), Some(b)) => o < b - 1e-12,
177            (Some(_), None) => true,
178            _ => false,
179        };
180        if !improves
181            && let Some(last) = guard.last_write
182            && now.duration_since(last) < MIN_CHECKPOINT_INTERVAL
183        {
184            return false;
185        }
186        match self.store.save_overwrite(
187            &self.key,
188            &self.run_id,
189            payload,
190            objective,
191            iteration,
192            EntryKind::Checkpoint,
193        ) {
194            Ok(()) => {
195                guard.last_write = Some(now);
196                if let Some(o) = objective {
197                    guard.best_seen = Some(match guard.best_seen {
198                        Some(b) => b.min(o),
199                        None => o,
200                    });
201                }
202                true
203            }
204            Err(error) => {
205                self.record_store_error("checkpoint outer-iterate session", &error);
206                false
207            }
208        }
209    }
210
211    /// Persist the end-of-fit result, promoting this session's slot to
212    /// `EntryKind::Final`. Bypasses the rate limit.
213    pub fn finalize(&self, payload: &[u8], objective: Option<f64>, iteration: Option<u64>) -> bool {
214        if !self.configured_store_is_available() {
215            return false;
216        }
217        match self.store.save_overwrite(
218            &self.key,
219            &self.run_id,
220            payload,
221            objective,
222            iteration,
223            EntryKind::Final,
224        ) {
225            Ok(()) => true,
226            Err(error) => {
227                self.record_store_error("finalize outer-iterate session", &error);
228                false
229            }
230        }
231    }
232}
233