Skip to main content

codex_state/runtime/
backfill.rs

1use super::*;
2
3impl StateRuntime {
4    pub async fn get_backfill_state(&self) -> anyhow::Result<crate::BackfillState> {
5        self.ensure_backfill_state_row().await?;
6        let row = sqlx::query(
7            r#"
8SELECT status, last_watermark, last_success_at
9FROM backfill_state
10WHERE id = 1
11            "#,
12        )
13        .fetch_one(self.pool.as_ref())
14        .await?;
15        crate::BackfillState::try_from_row(&row)
16    }
17
18    /// Attempt to claim ownership of rollout metadata backfill.
19    ///
20    /// Returns `true` when this runtime claimed the backfill worker slot.
21    /// Returns `false` if backfill is already complete or currently owned by a
22    /// non-expired worker.
23    pub async fn try_claim_backfill(&self, lease_seconds: i64) -> anyhow::Result<bool> {
24        self.ensure_backfill_state_row().await?;
25        let now = Utc::now().timestamp();
26        let lease_cutoff = now.saturating_sub(lease_seconds.max(0));
27        let result = sqlx::query(
28            r#"
29UPDATE backfill_state
30SET status = ?, updated_at = ?
31WHERE id = 1
32  AND status != ?
33  AND (status != ? OR updated_at <= ?)
34            "#,
35        )
36        .bind(crate::BackfillStatus::Running.as_str())
37        .bind(now)
38        .bind(crate::BackfillStatus::Complete.as_str())
39        .bind(crate::BackfillStatus::Running.as_str())
40        .bind(lease_cutoff)
41        .execute(self.pool.as_ref())
42        .await?;
43        Ok(result.rows_affected() == 1)
44    }
45
46    /// Mark rollout metadata backfill as running.
47    pub async fn mark_backfill_running(&self) -> anyhow::Result<()> {
48        self.ensure_backfill_state_row().await?;
49        sqlx::query(
50            r#"
51UPDATE backfill_state
52SET status = ?, updated_at = ?
53WHERE id = 1
54            "#,
55        )
56        .bind(crate::BackfillStatus::Running.as_str())
57        .bind(Utc::now().timestamp())
58        .execute(self.pool.as_ref())
59        .await?;
60        Ok(())
61    }
62
63    /// Persist rollout metadata backfill progress.
64    pub async fn checkpoint_backfill(&self, watermark: &str) -> anyhow::Result<()> {
65        self.ensure_backfill_state_row().await?;
66        sqlx::query(
67            r#"
68UPDATE backfill_state
69SET status = ?, last_watermark = ?, updated_at = ?
70WHERE id = 1
71            "#,
72        )
73        .bind(crate::BackfillStatus::Running.as_str())
74        .bind(watermark)
75        .bind(Utc::now().timestamp())
76        .execute(self.pool.as_ref())
77        .await?;
78        Ok(())
79    }
80
81    /// Mark rollout metadata backfill as complete.
82    pub async fn mark_backfill_complete(&self, last_watermark: Option<&str>) -> anyhow::Result<()> {
83        self.ensure_backfill_state_row().await?;
84        let now = Utc::now().timestamp();
85        sqlx::query(
86            r#"
87UPDATE backfill_state
88SET
89    status = ?,
90    last_watermark = COALESCE(?, last_watermark),
91    last_success_at = ?,
92    updated_at = ?
93WHERE id = 1
94            "#,
95        )
96        .bind(crate::BackfillStatus::Complete.as_str())
97        .bind(last_watermark)
98        .bind(now)
99        .bind(now)
100        .execute(self.pool.as_ref())
101        .await?;
102        Ok(())
103    }
104
105    async fn ensure_backfill_state_row(&self) -> anyhow::Result<()> {
106        ensure_backfill_state_row_in_pool(self.pool.as_ref()).await
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::StateRuntime;
113    use super::test_support::unique_temp_dir;
114    use chrono::Utc;
115    use codex_utils_absolute_path::test_support::PathExt;
116    use pretty_assertions::assert_eq;
117    use sqlx::Connection;
118
119    #[tokio::test]
120    async fn backfill_state_persists_progress_and_completion() {
121        let codex_home = unique_temp_dir();
122        let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
123            .await
124            .expect("initialize runtime");
125
126        let initial = runtime
127            .get_backfill_state()
128            .await
129            .expect("get initial backfill state");
130        assert_eq!(initial.status, crate::BackfillStatus::Pending);
131        assert_eq!(initial.last_watermark, None);
132        assert_eq!(initial.last_success_at, None);
133
134        runtime
135            .mark_backfill_running()
136            .await
137            .expect("mark backfill running");
138        runtime
139            .checkpoint_backfill("sessions/2026/01/27/rollout-a.jsonl")
140            .await
141            .expect("checkpoint backfill");
142
143        let running = runtime
144            .get_backfill_state()
145            .await
146            .expect("get running backfill state");
147        assert_eq!(running.status, crate::BackfillStatus::Running);
148        assert_eq!(
149            running.last_watermark,
150            Some("sessions/2026/01/27/rollout-a.jsonl".to_string())
151        );
152        assert_eq!(running.last_success_at, None);
153
154        runtime
155            .mark_backfill_complete(Some("sessions/2026/01/28/rollout-b.jsonl"))
156            .await
157            .expect("mark backfill complete");
158        let completed = runtime
159            .get_backfill_state()
160            .await
161            .expect("get completed backfill state");
162        assert_eq!(completed.status, crate::BackfillStatus::Complete);
163        assert_eq!(
164            completed.last_watermark,
165            Some("sessions/2026/01/28/rollout-b.jsonl".to_string())
166        );
167        assert!(completed.last_success_at.is_some());
168
169        let _ = tokio::fs::remove_dir_all(codex_home).await;
170    }
171
172    #[tokio::test]
173    async fn get_backfill_state_succeeds_while_another_connection_holds_writer_slot() {
174        let codex_home = unique_temp_dir();
175        let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
176            .await
177            .expect("initialize runtime");
178        let write_pool = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs())
179            .open_read_write_pool(&crate::state_db_path(codex_home.as_path()))
180            .await
181            .expect("open write pool");
182        let mut write_connection = write_pool.acquire().await.expect("open write connection");
183        let write_transaction = write_connection
184            .begin_with("BEGIN IMMEDIATE")
185            .await
186            .expect("acquire write lock");
187
188        let state = runtime
189            .get_backfill_state()
190            .await
191            .expect("get backfill state");
192        assert_eq!(state, crate::BackfillState::default());
193
194        write_transaction
195            .rollback()
196            .await
197            .expect("release write lock");
198        let _ = tokio::fs::remove_dir_all(codex_home).await;
199    }
200
201    #[tokio::test]
202    async fn get_backfill_state_repairs_a_missing_singleton_row() {
203        let codex_home = unique_temp_dir();
204        let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
205            .await
206            .expect("initialize runtime");
207        sqlx::query("DELETE FROM backfill_state WHERE id = 1")
208            .execute(runtime.pool.as_ref())
209            .await
210            .expect("delete backfill state row");
211
212        let state = runtime
213            .get_backfill_state()
214            .await
215            .expect("get repaired backfill state");
216        assert_eq!(state, crate::BackfillState::default());
217        let row_count =
218            sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM backfill_state WHERE id = 1")
219                .fetch_one(runtime.pool.as_ref())
220                .await
221                .expect("count repaired backfill state rows");
222        assert_eq!(row_count, 1);
223
224        let _ = tokio::fs::remove_dir_all(codex_home).await;
225    }
226
227    #[tokio::test]
228    async fn backfill_claim_is_singleton_until_stale_and_blocked_when_complete() {
229        let codex_home = unique_temp_dir();
230        let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
231            .await
232            .expect("initialize runtime");
233
234        let claimed = runtime
235            .try_claim_backfill(/*lease_seconds*/ 3600)
236            .await
237            .expect("initial backfill claim");
238        assert_eq!(claimed, true);
239
240        let duplicate_claim = runtime
241            .try_claim_backfill(/*lease_seconds*/ 3600)
242            .await
243            .expect("duplicate backfill claim");
244        assert_eq!(duplicate_claim, false);
245
246        let stale_updated_at = Utc::now().timestamp().saturating_sub(10_000);
247        sqlx::query(
248            r#"
249UPDATE backfill_state
250SET status = ?, updated_at = ?
251WHERE id = 1
252            "#,
253        )
254        .bind(crate::BackfillStatus::Running.as_str())
255        .bind(stale_updated_at)
256        .execute(runtime.pool.as_ref())
257        .await
258        .expect("force stale backfill lease");
259
260        let stale_claim = runtime
261            .try_claim_backfill(/*lease_seconds*/ 10)
262            .await
263            .expect("stale backfill claim");
264        assert_eq!(stale_claim, true);
265
266        runtime
267            .mark_backfill_complete(/*last_watermark*/ None)
268            .await
269            .expect("mark complete");
270        let claim_after_complete = runtime
271            .try_claim_backfill(/*lease_seconds*/ 3600)
272            .await
273            .expect("claim after complete");
274        assert_eq!(claim_after_complete, false);
275
276        let _ = tokio::fs::remove_dir_all(codex_home).await;
277    }
278}