#![allow(dead_code)]
#[cfg(any(feature = "postgres", feature = "mysql"))]
use hammerwork::{JobQueue, migrations::MigrationManager};
#[cfg(any(feature = "postgres", feature = "mysql"))]
use std::sync::Arc;
pub const DEFAULT_POSTGRES_URL: &str = "postgres://postgres:hammerwork@localhost:5433/hammerwork";
pub const DEFAULT_MYSQL_URL: &str = "mysql://root:hammerwork@localhost:3307/hammerwork";
pub fn postgres_url() -> String {
std::env::var("DATABASE_URL").unwrap_or_else(|_| DEFAULT_POSTGRES_URL.to_string())
}
pub fn mysql_url() -> String {
std::env::var("MYSQL_DATABASE_URL").unwrap_or_else(|_| DEFAULT_MYSQL_URL.to_string())
}
#[cfg(feature = "postgres")]
pub fn lazy_postgres_pool() -> sqlx::PgPool {
sqlx::PgPool::connect_lazy(&postgres_url()).expect("Invalid PostgreSQL URL")
}
#[cfg(feature = "postgres")]
pub async fn setup_postgres_queue() -> Arc<JobQueue<sqlx::Postgres>> {
use hammerwork::migrations::postgres::PostgresMigrationRunner;
use sqlx::{Pool, Postgres};
let pool = Pool::<Postgres>::connect(&postgres_url())
.await
.expect("Failed to connect to Postgres");
let queue = Arc::new(JobQueue::new(pool.clone()));
let runner = Box::new(PostgresMigrationRunner::new(pool));
let manager = MigrationManager::new(runner);
manager
.run_migrations()
.await
.expect("Failed to run migrations");
queue
}
#[cfg(feature = "mysql")]
pub async fn setup_mysql_queue() -> Arc<JobQueue<sqlx::MySql>> {
use hammerwork::migrations::mysql::MySqlMigrationRunner;
use sqlx::{MySql, Pool};
let pool = Pool::<MySql>::connect(&mysql_url())
.await
.expect("Failed to connect to MySQL");
let queue = Arc::new(JobQueue::new(pool.clone()));
let runner = Box::new(MySqlMigrationRunner::new(pool));
let manager = MigrationManager::new(runner);
manager
.run_migrations()
.await
.expect("Failed to run migrations");
queue
}
pub fn unique_queue(prefix: &str) -> String {
format!("{prefix}_{}", uuid::Uuid::new_v4().simple())
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
pub struct QueueCleanup {
delete: fn(String, String) -> Result<(), String>,
url: String,
queue_name: String,
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
impl QueueCleanup {
#[cfg(feature = "postgres")]
pub fn postgres(queue_name: &str) -> Self {
Self {
delete: |url, queue_name| {
block_on_own_runtime(async move {
use sqlx::Connection;
let mut conn = sqlx::PgConnection::connect(&url).await?;
sqlx::query("DELETE FROM hammerwork_jobs WHERE queue_name = $1")
.bind(queue_name)
.execute(&mut conn)
.await?;
Ok(())
})
},
url: postgres_url(),
queue_name: queue_name.to_string(),
}
}
#[cfg(feature = "mysql")]
pub fn mysql(queue_name: &str) -> Self {
Self {
delete: |url, queue_name| {
block_on_own_runtime(async move {
use sqlx::Connection;
let mut conn = sqlx::MySqlConnection::connect(&url).await?;
sqlx::query("DELETE FROM hammerwork_jobs WHERE queue_name = ?")
.bind(queue_name)
.execute(&mut conn)
.await?;
Ok(())
})
},
url: mysql_url(),
queue_name: queue_name.to_string(),
}
}
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
fn block_on_own_runtime(
future: impl std::future::Future<Output = Result<(), sqlx::Error>>,
) -> Result<(), String> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| e.to_string())?
.block_on(future)
.map_err(|e| e.to_string())
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
impl Drop for QueueCleanup {
fn drop(&mut self) {
let (delete, url, queue_name) = (self.delete, self.url.clone(), self.queue_name.clone());
let cleaned = std::thread::spawn(move || delete(url, queue_name)).join();
match cleaned {
Ok(Ok(_)) => {}
Ok(Err(e)) => eprintln!("cleaning up queue {}: {e}", self.queue_name),
Err(_) => eprintln!("cleaning up queue {} panicked", self.queue_name),
}
}
}
static SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
pub async fn serial() -> tokio::sync::MutexGuard<'static, ()> {
SERIAL.lock().await
}
#[cfg(feature = "test")]
pub async fn test_queue() -> std::sync::Arc<hammerwork::queue::test::TestQueue> {
std::sync::Arc::new(hammerwork::queue::test::TestQueue::new())
}
#[allow(unused_macros)]
macro_rules! backend_tests {
(#[$test:meta] $module:ident, $setup:path, $ignore:meta, [$($scenario:ident),* $(,)?]) => {
mod $module {
use super::*;
$(
#[$test]
#[$ignore]
async fn $scenario() {
super::$scenario($setup().await).await;
}
)*
}
};
($module:ident, $setup:path, $ignore:meta, [$($scenario:ident),* $(,)?]) => {
$crate::test_utils::backend_tests!(
#[tokio::test] $module, $setup, $ignore, [$($scenario),*]
);
};
}
#[allow(unused_imports)]
pub(crate) use backend_tests;
pub async fn wait_for_job<Q, F>(
queue: &std::sync::Arc<Q>,
id: hammerwork::JobId,
timeout: std::time::Duration,
predicate: F,
) -> hammerwork::Job
where
Q: hammerwork::queue::DatabaseQueue + ?Sized,
F: Fn(&hammerwork::Job) -> bool,
{
let deadline = std::time::Instant::now() + timeout;
loop {
let job = queue.get_job(id).await.unwrap().expect("job exists");
if predicate(&job) || std::time::Instant::now() >= deadline {
return job;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
pub async fn eventually<F, Fut>(timeout: std::time::Duration, mut condition: F) -> bool
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = bool>,
{
let deadline = std::time::Instant::now() + timeout;
loop {
if condition().await {
return true;
}
if std::time::Instant::now() >= deadline {
return false;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
pub fn idle_worker<DB>(queue: &Arc<JobQueue<DB>>, prefix: &str) -> hammerwork::Worker<DB>
where
DB: sqlx::Database + Send + Sync + 'static,
JobQueue<DB>: hammerwork::queue::DatabaseQueue<Database = DB> + Send + Sync + 'static,
{
let handler: hammerwork::worker::JobHandler =
Arc::new(|_job: hammerwork::Job| Box::pin(async { Ok(()) }));
hammerwork::Worker::new(Arc::clone(queue), unique_queue(prefix), handler)
.with_poll_interval(std::time::Duration::from_millis(50))
}
#[allow(unused_macros)]
macro_rules! scenario_tests {
($setup:path; $($(#[$attr:meta])* $name:ident => $scenario:path),* $(,)?) => {
$(
$(#[$attr])*
#[tokio::test]
#[ignore] // Requires database connection
async fn $name() {
$scenario($setup().await).await;
}
)*
};
}
#[allow(unused_imports)]
pub(crate) use scenario_tests;