1use std::{collections::HashSet, env, time::Duration};
2
3use async_trait::async_trait;
4
5use crate::data_source::{
6 Transaction as _, VersionedDataSource,
7 storage::sql::{Backfill, SqlStorage, Transaction},
8};
9
10const RETRY_INTERVAL: Duration = Duration::from_secs(300);
11
12#[async_trait]
14pub trait DataBackfill: Send + Sync + 'static {
15 fn name(&self) -> &'static str;
16
17 fn requires(&self) -> &'static [&'static str] {
19 &[]
20 }
21
22 fn batch_size(&self) -> usize {
24 1_000
25 }
26
27 fn log_interval(&self) -> usize {
29 10
30 }
31
32 fn batch_delay(&self) -> Duration {
34 Duration::from_millis(50)
35 }
36
37 async fn run_batch(
44 &self,
45 tx: &mut Transaction<Backfill>,
46 offset: u64,
47 ) -> anyhow::Result<Option<u64>>;
48}
49
50pub struct MigrationRegistry {
52 migrations: Vec<Box<dyn DataBackfill>>,
53}
54
55impl MigrationRegistry {
56 pub fn new() -> Self {
58 Self {
59 migrations: Vec::new(),
60 }
61 }
62
63 pub fn backfill(mut self, m: impl DataBackfill) -> Self {
65 self.migrations.push(Box::new(m));
66 self
67 }
68
69 pub fn validate(&self) -> anyhow::Result<()> {
75 let mut seen: HashSet<&'static str> = HashSet::new();
76 for m in &self.migrations {
77 let name = m.name();
78 anyhow::ensure!(seen.insert(name), "duplicate migration name: {name}");
79 for dep in m.requires() {
80 anyhow::ensure!(
81 seen.contains(dep),
82 "migration {name} requires {dep} which either does not exist or appears later \
83 in the registry",
84 );
85 }
86 }
87 Ok(())
88 }
89
90 pub fn iter(&self) -> impl Iterator<Item = &dyn DataBackfill> {
92 self.migrations.iter().map(|b| b.as_ref())
93 }
94
95 pub async fn run_all_migrations(self, db: SqlStorage) {
101 if let Err(e) = self.validate() {
102 tracing::error!(
103 "deferred migration registry is invalid, skipping all backfills: {e:#}"
104 );
105 return;
106 }
107
108 loop {
109 let mut pending = 0usize;
110
111 for m in &self.migrations {
112 let name = m.name();
113
114 match Self::is_complete(&db, name).await {
116 Ok(true) => continue,
117 Ok(false) => {},
118 Err(e) => {
119 tracing::error!(name, "failed to check migration status: {e:#}");
120 pending += 1;
121 continue;
122 },
123 }
124
125 let mut deps_ready = true;
127 for dep in m.requires() {
128 match Self::is_complete(&db, dep).await {
129 Ok(true) => {},
130 Ok(false) => {
131 tracing::warn!(
132 name,
133 dep,
134 "prerequisite not yet complete, will retry after \
135 {RETRY_INTERVAL:?}"
136 );
137 deps_ready = false;
138 },
139 Err(e) => {
140 tracing::error!(name, dep, "failed to check prerequisite: {e:#}");
141 deps_ready = false;
142 },
143 }
144 }
145 if !deps_ready {
146 pending += 1;
147 continue;
148 }
149
150 if let Err(e) = Self::run_migration(&db, m.as_ref()).await {
152 tracing::error!(name, "deferred migration failed: {e:#}");
153 pending += 1;
154 }
155 }
156
157 if pending == 0 {
158 tracing::warn!("all deferred migrations complete");
159 break;
160 }
161
162 tokio::time::sleep(RETRY_INTERVAL).await;
163 }
164 }
165
166 async fn run_migration(db: &SqlStorage, m: &dyn DataBackfill) -> anyhow::Result<()> {
168 use anyhow::Context as _;
169
170 let name = m.name();
171
172 let mut offset = Self::init_and_get_offset(db, name)
173 .await
174 .context("failed to initialize migration")?;
175
176 let delay = env::var("ESPRESSO_NODE_BACKFILL_BATCH_DELAY_MS")
177 .ok()
178 .and_then(|v| v.parse::<u64>().ok())
179 .map(Duration::from_millis)
180 .unwrap_or_else(|| m.batch_delay());
181
182 tracing::warn!(name, offset, ?delay, "starting deferred migration");
183
184 let mut batch_count: usize = 0;
185
186 loop {
187 let mut tx = db.backfill().await.with_context(|| {
188 format!("failed to open backfill transaction at offset {offset}")
189 })?;
190
191 let next = m
192 .run_batch(&mut tx, offset)
193 .await
194 .with_context(|| format!("migration batch failed at offset {offset}"))?;
195
196 sqlx::query(
197 "UPDATE deferred_migrations SET last_offset = $1, completed_at = CASE WHEN $2 \
198 THEN CURRENT_TIMESTAMP ELSE completed_at END WHERE name = $3",
199 )
200 .bind(next.unwrap_or(offset) as i64)
201 .bind(next.is_none())
202 .bind(name)
203 .execute(tx.as_mut())
204 .await
205 .context("failed to persist migration progress")?;
206
207 tx.commit()
208 .await
209 .context("failed to commit migration batch")?;
210
211 batch_count += 1;
212
213 let Some(next_offset) = next else {
214 tracing::warn!(name, batches = batch_count, "deferred migration complete");
215 return Ok(());
216 };
217 offset = next_offset;
218
219 if !delay.is_zero() {
220 tokio::time::sleep(delay).await;
221 }
222
223 if batch_count.is_multiple_of(m.log_interval()) {
224 tracing::warn!(
225 name,
226 offset,
227 batches = batch_count,
228 "deferred migration progress"
229 );
230 }
231 }
232 }
233
234 async fn is_complete(db: &SqlStorage, name: &str) -> anyhow::Result<bool> {
237 let mut tx = db.read().await?;
238 let row: Option<(bool,)> = sqlx::query_as(
239 "SELECT completed_at IS NOT NULL FROM deferred_migrations WHERE name = $1",
240 )
241 .bind(name)
242 .fetch_optional(tx.as_mut())
243 .await?;
244 Ok(row.map(|(b,)| b).unwrap_or(false))
245 }
246
247 async fn init_and_get_offset(db: &SqlStorage, name: &str) -> anyhow::Result<u64> {
250 let mut tx = db.write().await?;
251
252 sqlx::query(
253 "INSERT INTO deferred_migrations (name, started_at, last_offset) VALUES ($1, \
254 CURRENT_TIMESTAMP, 0) ON CONFLICT (name) DO NOTHING",
255 )
256 .bind(name)
257 .execute(tx.as_mut())
258 .await?;
259
260 let (last_offset,): (i64,) = sqlx::query_as(
261 "SELECT COALESCE(last_offset, 0) FROM deferred_migrations WHERE name = $1",
262 )
263 .bind(name)
264 .fetch_one(tx.as_mut())
265 .await?;
266
267 tx.commit().await?;
268
269 Ok(last_offset as u64)
270 }
271}
272
273impl Default for MigrationRegistry {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 struct CountBatches {
285 name: &'static str,
286 total: u64,
287 deps: &'static [&'static str],
288 }
289
290 impl CountBatches {
291 fn new(name: &'static str, total: u64) -> Self {
292 Self {
293 name,
294 total,
295 deps: &[],
296 }
297 }
298
299 fn with_deps(name: &'static str, total: u64, deps: &'static [&'static str]) -> Self {
300 Self { name, total, deps }
301 }
302 }
303
304 #[async_trait]
305 impl DataBackfill for CountBatches {
306 fn name(&self) -> &'static str {
307 self.name
308 }
309
310 fn requires(&self) -> &'static [&'static str] {
311 self.deps
312 }
313
314 fn batch_size(&self) -> usize {
315 1
316 }
317
318 async fn run_batch(
319 &self,
320 _tx: &mut Transaction<Backfill>,
321 offset: u64,
322 ) -> anyhow::Result<Option<u64>> {
323 Ok((offset < self.total).then_some(offset + 1))
324 }
325 }
326
327 #[test]
330 fn validate_empty() {
331 MigrationRegistry::new().validate().unwrap();
332 }
333
334 #[test]
335 fn validate_single() {
336 MigrationRegistry::new()
337 .backfill(CountBatches::new("a", 1))
338 .validate()
339 .unwrap();
340 }
341
342 #[test]
343 fn validate_duplicate_name() {
344 let err = MigrationRegistry::new()
345 .backfill(CountBatches::new("foo", 1))
346 .backfill(CountBatches::new("foo", 1))
347 .validate()
348 .unwrap_err();
349 assert!(err.to_string().contains("duplicate"));
350 }
351
352 #[test]
353 fn validate_unknown_dep() {
354 let err = MigrationRegistry::new()
355 .backfill(CountBatches::with_deps("bar", 1, &["nonexistent"]))
356 .validate()
357 .unwrap_err();
358 assert!(err.to_string().contains("nonexistent"));
359 }
360
361 #[test]
362 fn validate_dep_must_precede() {
363 let err = MigrationRegistry::new()
365 .backfill(CountBatches::with_deps("a", 1, &["b"]))
366 .backfill(CountBatches::new("b", 1))
367 .validate()
368 .unwrap_err();
369 assert!(err.to_string().contains("b"));
370 }
371
372 #[test]
373 fn validate_valid_dependency_chain() {
374 MigrationRegistry::new()
375 .backfill(CountBatches::new("a", 1))
376 .backfill(CountBatches::with_deps("b", 1, &["a"]))
377 .backfill(CountBatches::with_deps("c", 1, &["a", "b"]))
378 .validate()
379 .unwrap();
380 }
381
382 #[cfg(not(feature = "embedded-db"))]
385 mod db_tests {
386 use super::*;
387 use crate::data_source::storage::sql::{
388 Migration, SqlStorage, StorageConnectionType, testing::TmpDb,
389 };
390
391 const CREATE_DEFERRED_MIGRATIONS: &str = "
394 CREATE TABLE IF NOT EXISTS deferred_migrations (
395 name TEXT PRIMARY KEY,
396 started_at TIMESTAMPTZ NOT NULL,
397 completed_at TIMESTAMPTZ,
398 last_offset BIGINT
399 );
400 ";
401
402 async fn setup() -> (TmpDb, SqlStorage) {
403 let db = TmpDb::init().await;
404 let storage = SqlStorage::connect(
405 db.config().migrations(vec![
406 Migration::unapplied(
407 "V9990__deferred_migrations.sql",
408 CREATE_DEFERRED_MIGRATIONS,
409 )
410 .unwrap(),
411 ]),
412 StorageConnectionType::Query,
413 )
414 .await
415 .unwrap();
416 (db, storage)
417 }
418
419 async fn is_complete(storage: &SqlStorage, name: &str) -> bool {
420 let mut tx = storage.read().await.unwrap();
421 let row: Option<(bool,)> = sqlx::query_as(
422 "SELECT completed_at IS NOT NULL FROM deferred_migrations WHERE name = $1",
423 )
424 .bind(name)
425 .fetch_optional(tx.as_mut())
426 .await
427 .unwrap();
428 row.map(|(b,)| b).unwrap_or(false)
429 }
430
431 async fn last_offset(storage: &SqlStorage, name: &str) -> Option<i64> {
432 let mut tx = storage.read().await.unwrap();
433 let row: Option<(i64,)> =
434 sqlx::query_as("SELECT last_offset FROM deferred_migrations WHERE name = $1")
435 .bind(name)
436 .fetch_optional(tx.as_mut())
437 .await
438 .unwrap();
439 row.map(|(o,)| o)
440 }
441
442 #[test_log::test(tokio::test(flavor = "multi_thread"))]
443 async fn migration_runs_to_completion() {
444 let (_db, storage) = setup().await;
445
446 MigrationRegistry::new()
447 .backfill(CountBatches::new("m", 5))
448 .run_all_migrations(storage.clone())
449 .await;
450
451 assert!(is_complete(&storage, "m").await);
452 assert_eq!(last_offset(&storage, "m").await, Some(5));
453 }
454
455 #[test_log::test(tokio::test(flavor = "multi_thread"))]
456 async fn migration_resumes_from_checkpoint() {
457 let (_db, storage) = setup().await;
458
459 let mut tx = storage.write().await.unwrap();
461 sqlx::query(
462 "INSERT INTO deferred_migrations (name, started_at, last_offset) VALUES ($1, \
463 CURRENT_TIMESTAMP, $2)",
464 )
465 .bind("m")
466 .bind(3i64)
467 .execute(tx.as_mut())
468 .await
469 .unwrap();
470 tx.commit().await.unwrap();
471
472 MigrationRegistry::new()
473 .backfill(CountBatches::new("m", 5))
474 .run_all_migrations(storage.clone())
475 .await;
476
477 assert!(is_complete(&storage, "m").await);
478 assert_eq!(last_offset(&storage, "m").await, Some(5));
479 }
480
481 #[test_log::test(tokio::test(flavor = "multi_thread"))]
482 async fn completed_migration_is_skipped() {
483 let (_db, storage) = setup().await;
484
485 let mut tx = storage.write().await.unwrap();
487 sqlx::query(
488 "INSERT INTO deferred_migrations (name, started_at, completed_at, last_offset) \
489 VALUES ($1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $2)",
490 )
491 .bind("m")
492 .bind(99i64)
493 .execute(tx.as_mut())
494 .await
495 .unwrap();
496 tx.commit().await.unwrap();
497
498 MigrationRegistry::new()
500 .backfill(CountBatches::new("m", 0))
501 .run_all_migrations(storage.clone())
502 .await;
503
504 assert_eq!(last_offset(&storage, "m").await, Some(99));
506 }
507
508 #[test_log::test(tokio::test(flavor = "multi_thread"))]
509 async fn dependency_ordering_respected() {
510 let (_db, storage) = setup().await;
511
512 MigrationRegistry::new()
513 .backfill(CountBatches::new("first", 3))
514 .backfill(CountBatches::with_deps("second", 3, &["first"]))
515 .run_all_migrations(storage.clone())
516 .await;
517
518 assert!(is_complete(&storage, "first").await);
519 assert!(is_complete(&storage, "second").await);
520 }
521 }
522}