codex_state/runtime/
backfill.rs1use 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 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 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 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 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(3600)
236 .await
237 .expect("initial backfill claim");
238 assert_eq!(claimed, true);
239
240 let duplicate_claim = runtime
241 .try_claim_backfill(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(10)
262 .await
263 .expect("stale backfill claim");
264 assert_eq!(stale_claim, true);
265
266 runtime
267 .mark_backfill_complete(None)
268 .await
269 .expect("mark complete");
270 let claim_after_complete = runtime
271 .try_claim_backfill(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}