#![cfg(all(feature = "sqlite", feature = "test-support"))]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use autumn_web::app::AppBuilder;
use autumn_web::job;
use autumn_web::migrate::{EmbeddedMigrations, embed_migrations};
use autumn_web::plugin::Plugin;
use autumn_web::prelude::*;
use autumn_web::sim::Sim;
use autumn_web::sim::substrate::SqliteSubstrate;
use autumn_web::sim_test;
use autumn_web::test::TestApp;
use autumn_web::time::Clock;
use serde::{Deserialize, Serialize};
const TWENTY_FOUR_HOURS: Duration = Duration::from_secs(24 * 3600);
const MARK_MIGRATIONS: EmbeddedMigrations =
embed_migrations!("tests/fixtures/sim_sqlite_substrate");
static MARK_RUNS: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct MarkArgs {
id: i64,
tag: String,
}
#[job(name = "sim_delayed_mark", max_attempts = 2, backoff_ms = 86_400_000)]
async fn sim_delayed_mark(state: AppState, args: MarkArgs) -> AutumnResult<()> {
use diesel_async::RunQueryDsl as _;
let prior = MARK_RUNS.fetch_add(1, Ordering::SeqCst);
if prior == 0 {
return Err(AutumnError::internal_server_error(std::io::Error::other(
"first attempt fails to force the 24h retry backoff",
)));
}
let pool = state
.pool()
.expect("substrate pool must be wired into AppState via TestApp::with_db")
.clone();
let mut conn = pool
.get()
.await
.map_err(|e| AutumnError::internal_server_error(std::io::Error::other(e.to_string())))?;
diesel::sql_query("INSERT INTO sim_job_marks (id, tag) VALUES (?, ?)")
.bind::<diesel::sql_types::BigInt, _>(args.id)
.bind::<diesel::sql_types::Text, _>(args.tag)
.execute(&mut *conn)
.await
.map_err(|e| AutumnError::internal_server_error(std::io::Error::other(e.to_string())))?;
Ok(())
}
struct SimJobsPlugin;
impl Plugin for SimJobsPlugin {
fn build(self, app: AppBuilder) -> AppBuilder {
app.jobs(jobs![sim_delayed_mark])
}
}
#[get("/now")]
async fn now_route(clock: Clock) -> String {
clock.now().to_rfc3339()
}
#[derive(diesel::QueryableByName)]
struct MarkRow {
#[diesel(sql_type = diesel::sql_types::Text)]
tag: String,
}
#[allow(clippy::future_not_send)]
async fn recorded_tags(substrate: &SqliteSubstrate) -> Vec<String> {
use diesel_async::RunQueryDsl as _;
let pool = substrate.pool();
let mut conn = pool.get().await.expect("checkout substrate connection");
let rows: Vec<MarkRow> = diesel::sql_query("SELECT tag FROM sim_job_marks ORDER BY id")
.load(&mut *conn)
.await
.expect("read sim_job_marks from the migrated SQLite schema");
rows.into_iter().map(|r| r.tag).collect()
}
#[sim_test]
async fn w2_drain_runs_against_w4_sqlite_substrate(mut sim: Sim) {
let _guard = job::global_job_runtime_test_lock().lock().await;
job::clear_global_job_client();
MARK_RUNS.store(0, Ordering::SeqCst);
let wall_start = Instant::now();
let substrate =
SqliteSubstrate::with_migrations(&[&MARK_MIGRATIONS]).expect("migrated substrate builds");
assert!(
recorded_tags(&substrate).await.is_empty(),
"no rows before the drain"
);
sim.build(
TestApp::new()
.plugin(SimJobsPlugin)
.with_db(substrate.pool())
.routes(routes![now_route]),
);
let before = sim.client().get("/now").send().await;
before.assert_ok();
assert_eq!(before.text(), "2020-01-01T00:00:00+00:00");
SimDelayedMarkJob::enqueue(MarkArgs {
id: 1,
tag: "drained".to_owned(),
})
.await
.expect("enqueue via the local job backend");
sim.client().assert_job_enqueued("sim_delayed_mark");
sim.run_to_idle().await;
assert_eq!(
MARK_RUNS.load(Ordering::SeqCst),
1,
"attempt 1 should have run once and failed"
);
assert!(
recorded_tags(&substrate).await.is_empty(),
"the failed first attempt must not have written a row — the retry is still backing off in virtual time"
);
sim.advance(TWENTY_FOUR_HOURS).await;
sim.run_to_idle().await;
assert_eq!(
MARK_RUNS.load(Ordering::SeqCst),
2,
"the 24h-backoff retry should have fired and succeeded in virtual time"
);
assert_eq!(
recorded_tags(&substrate).await,
vec!["drained".to_owned()],
"the W2 drain wrote exactly one row to the migrated W4 SQLite schema via the Sim API"
);
let after = sim.client().get("/now").send().await;
after.assert_ok();
assert_eq!(after.text(), "2020-01-02T00:00:00+00:00");
let wall_elapsed = wall_start.elapsed();
assert!(
wall_elapsed < Duration::from_secs(60),
"the 24h virtual backoff must not sleep on the real clock (a real backoff \
would take 24h); {wall_elapsed:?} elapsed"
);
job::clear_global_job_client();
}