#![allow(dead_code)]
use crate::JobStorage; use crate::error::{JobError, JobResult};
use crate::job_executor::{JobExecutor, MockQueueService};
use crate::scheduler::{JobScheduler, SchedulerConfig};
use backbone_queue::{QueueService, RedisQueue, RedisQueueConfig, RabbitMQQueueSimple, RabbitMQConfig};
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Duration;
pub struct JobSchedulerBuilder {
storage: Option<Arc<dyn JobStorage>>,
executor: Option<JobExecutor>,
config: SchedulerConfig,
database_url: Option<String>,
queue_service: Option<Arc<dyn QueueService>>,
}
impl JobSchedulerBuilder {
pub fn new() -> Self {
Self {
storage: None,
executor: None,
config: SchedulerConfig::default(),
database_url: None,
queue_service: None,
}
}
pub fn database_url<S: Into<String>>(mut self, url: S) -> Self {
self.database_url = Some(url.into());
self
}
pub fn database_pool(self, _pool: PgPool) -> Self {
self
}
pub fn storage(mut self, storage: Arc<dyn JobStorage>) -> Self {
self.storage = Some(storage);
self
}
pub fn queue_service(mut self, service: Arc<dyn QueueService>) -> Self {
self.queue_service = Some(service);
self
}
pub fn redis_queue<S: Into<String>>(self, url: S) -> JobResult<Self> {
let config = RedisQueueConfig {
url: url.into(),
..Default::default()
};
self.redis_queue_with_config(config)
}
pub fn redis_queue_with_config(self, config: RedisQueueConfig) -> JobResult<Self> {
tracing::info!("Redis queue configured with URL: {}", config.url);
Ok(self)
}
pub fn rabbitmq_queue<S: Into<String>>(self, connection_url: S) -> JobResult<Self> {
let config = RabbitMQConfig {
connection_url: connection_url.into(),
..Default::default()
};
self.rabbitmq_queue_with_config(config)
}
pub fn rabbitmq_queue_with_config(self, config: RabbitMQConfig) -> JobResult<Self> {
tracing::info!("RabbitMQ queue configured with URL: {}", config.connection_url);
Ok(self)
}
pub fn poll_interval(mut self, interval: Duration) -> Self {
self.config.poll_interval = interval;
self
}
pub fn poll_interval_seconds(self, seconds: u64) -> Self {
self.poll_interval(Duration::from_secs(seconds))
}
pub fn max_concurrent_jobs(mut self, max: usize) -> Self {
self.config.max_concurrent_jobs = max;
self
}
pub fn default_timeout(mut self, timeout: Duration) -> Self {
self.config.default_timeout = timeout;
self
}
pub fn default_timeout_seconds(self, seconds: u64) -> Self {
self.default_timeout(Duration::from_secs(seconds))
}
pub fn default_timezone<S: Into<String>>(mut self, timezone: S) -> Self {
self.config.default_timezone = timezone.into();
self
}
pub fn auto_start(mut self, auto_start: bool) -> Self {
self.config.auto_start = auto_start;
self
}
pub fn cleanup_old_attempts(mut self, cleanup: bool) -> Self {
self.config.cleanup_old_attempts = cleanup;
self
}
pub fn cleanup_attempts_older_than_days(mut self, days: i64) -> Self {
self.config.cleanup_attempts_older_than_days = days;
self
}
pub async fn build_in_memory(self) -> JobResult<JobScheduler> {
let storage = Arc::new(crate::job_storage::in_memory::InMemoryJobStorage::new());
let queue_service = Arc::new(MockQueueService::default());
let executor = JobExecutor::new(queue_service, self.config.default_timeout);
Ok(JobScheduler::new(storage, executor, self.config))
}
pub async fn build(self) -> JobResult<JobScheduler> {
let storage = self.storage.ok_or_else(|| {
JobError::configuration("Database URL or storage is required")
})?;
let queue_service = self.queue_service.unwrap_or_else(|| {
Arc::new(MockQueueService::default())
});
let executor = JobExecutor::new(queue_service, self.config.default_timeout);
Ok(JobScheduler::new(storage, executor, self.config))
}
pub async fn build_with_redis<S: Into<String>>(self, redis_url: S) -> JobResult<JobScheduler> {
let storage = self.storage.ok_or_else(|| {
JobError::configuration("Database URL or storage is required")
})?;
let redis_config = RedisQueueConfig {
url: redis_url.into(),
..Default::default()
};
let redis_queue = RedisQueue::new(redis_config)
.await
.map_err(|e| JobError::queue_service(&format!("Redis connection error: {}", e)))?;
let queue_service: Arc<dyn QueueService> = Arc::new(redis_queue);
let executor = JobExecutor::new(queue_service, self.config.default_timeout);
Ok(JobScheduler::new(storage, executor, self.config))
}
pub async fn build_with_rabbitmq<S: Into<String>>(self, rabbitmq_url: S) -> JobResult<JobScheduler> {
let storage = self.storage.ok_or_else(|| {
JobError::configuration("Database URL or storage is required")
})?;
let rabbitmq_config = RabbitMQConfig {
connection_url: rabbitmq_url.into(),
..Default::default()
};
let rabbitmq_queue = RabbitMQQueueSimple::new(rabbitmq_config)
.await
.map_err(|e| JobError::queue_service(&format!("RabbitMQ connection error: {}", e)))?;
let queue_service: Arc<dyn QueueService> = Arc::new(rabbitmq_queue);
let executor = JobExecutor::new(queue_service, self.config.default_timeout);
Ok(JobScheduler::new(storage, executor, self.config))
}
}
impl Default for JobSchedulerBuilder {
fn default() -> Self {
Self::new()
}
}
pub trait JobSchedulerExt {
fn from_database_url<S: Into<String>>(url: S) -> JobSchedulerBuilder;
fn from_database_pool(pool: PgPool) -> JobSchedulerBuilder;
}
impl JobSchedulerExt for JobSchedulerBuilder {
fn from_database_url<S: Into<String>>(url: S) -> JobSchedulerBuilder {
JobSchedulerBuilder::new().database_url(url)
}
fn from_database_pool(pool: PgPool) -> JobSchedulerBuilder {
JobSchedulerBuilder::new().database_pool(pool)
}
}
pub mod convenience {
use super::*;
pub async fn create_in_memory_scheduler() -> JobResult<JobScheduler> {
JobSchedulerBuilder::new().build_in_memory().await
}
pub async fn create_postgres_scheduler<S: Into<String>>(_database_url: S) -> JobResult<JobScheduler> {
Err(JobError::configuration("PostgreSQL storage temporarily disabled"))
}
pub async fn create_production_scheduler<S: Into<String>>(
database_url: S,
redis_url: S,
) -> JobResult<JobScheduler> {
JobSchedulerBuilder::new()
.database_url(database_url)
.build_with_redis(redis_url)
.await
}
pub async fn create_high_throughput_scheduler<S: Into<String>>(
database_url: S,
rabbitmq_url: S,
) -> JobResult<JobScheduler> {
JobSchedulerBuilder::new()
.database_url(database_url)
.build_with_rabbitmq(rabbitmq_url)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_defaults() {
let builder = JobSchedulerBuilder::new();
assert_eq!(builder.config.poll_interval, Duration::from_secs(60));
assert_eq!(builder.config.max_concurrent_jobs, 10);
assert_eq!(builder.config.default_timeout, Duration::from_secs(300));
assert_eq!(builder.config.default_timezone, "UTC");
assert!(builder.config.auto_start);
}
#[test]
fn test_builder_configuration() {
let builder = JobSchedulerBuilder::new()
.poll_interval_seconds(30)
.max_concurrent_jobs(20)
.default_timeout_seconds(600)
.default_timezone("America/New_York")
.auto_start(false)
.cleanup_old_attempts(false);
assert_eq!(builder.config.poll_interval, Duration::from_secs(30));
assert_eq!(builder.config.max_concurrent_jobs, 20);
assert_eq!(builder.config.default_timeout, Duration::from_secs(600));
assert_eq!(builder.config.default_timezone, "America/New_York");
assert!(!builder.config.auto_start);
assert!(!builder.config.cleanup_old_attempts);
}
#[tokio::test]
async fn test_in_memory_builder() {
let builder = JobSchedulerBuilder::new();
let scheduler = builder.build_in_memory().await;
assert!(scheduler.is_ok());
}
}