use anyhow::Result;
use tokio_cron_scheduler::{Job, JobScheduler};
use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::http::Client;
use surrealdb::Surreal;
use crate::run_crawler;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CrawlerJob {
pub name: String,
pub cron: String, pub url: String,
pub deep_scan: bool,
pub active: bool,
pub client_id: String,
pub project_id: String,
pub domain: String,
}
pub struct SchedulerManager {
db: Surreal<Client>,
scheduler: JobScheduler,
}
impl SchedulerManager {
pub async fn new() -> Result<Self> {
let db = Surreal::new::<surrealdb::engine::remote::http::Http>("localhost:8000").await?;
db.signin(surrealdb::opt::auth::Root {
username: "root".into(),
password: "root".into(),
}).await?;
db.use_ns("bgust").use_db("ecosystem").await?;
let scheduler = JobScheduler::new().await?;
Ok(Self { db, scheduler })
}
pub async fn seed_default_job_if_needed(&self) -> Result<()> {
let jobs_val: Vec<serde_json::Value> = self.db.select("crawler_jobs").await?;
if jobs_val.is_empty() {
println!("⏰ [SCHEDULER] No jobs found in SurrealDB. Seeding default BCV task...");
let default_job = CrawlerJob {
name: "tasas_bcv".to_string(),
cron: "0 0 9 * * Mon-Fri *".to_string(), url: "https://www.bcv.org.ve/".to_string(),
deep_scan: false,
active: true,
client_id: "client_default".to_string(),
project_id: "project_default".to_string(),
domain: "finance".to_string(),
};
let _: Option<serde_json::Value> = self.db.create(("crawler_jobs", default_job.name.as_str()))
.content(serde_json::to_value(&default_job)?)
.await?;
println!("⏰ [SCHEDULER] Seeded job: tasas_bcv");
}
Ok(())
}
pub async fn start(&self) -> Result<()> {
self.seed_default_job_if_needed().await?;
let mut query_res = self.db.query("SELECT * FROM crawler_jobs WHERE active = true").await?;
let active_jobs_val: Vec<serde_json::Value> = query_res.take(0)?;
let mut active_jobs = Vec::new();
for val in active_jobs_val {
if let Ok(job) = serde_json::from_value::<CrawlerJob>(val) {
active_jobs.push(job);
}
}
println!("⏰ [SCHEDULER] Found {} active jobs in SurrealDB.", active_jobs.len());
for job in active_jobs {
let name = job.name.clone();
let url = job.url.clone();
let deep_scan = job.deep_scan;
let cron_expr = job.cron.clone();
let client_id = job.client_id.clone();
let project_id = job.project_id.clone();
let domain = job.domain.clone();
println!("⏰ [SCHEDULER] Scheduling job: {} with cron: {}", name, cron_expr);
let name_err = name.clone();
let job_runner = Job::new_async(cron_expr.as_str(), move |_uuid, _l| {
let url = url.clone();
let name = name.clone();
let client_id = client_id.clone();
let project_id = project_id.clone();
let domain = domain.clone();
Box::pin(async move {
println!("⏰ [SCHEDULER] Running scheduled job '{}' for url '{}'...", name, url);
match run_crawler(&url, deep_scan, Some(&client_id), Some(&project_id), Some(&domain)).await {
Ok(ptr) => println!("⏰ [SCHEDULER] Job '{}' completed successfully. Saved to: {}", name, ptr),
Err(e) => eprintln!("⚠️ [SCHEDULER] Job '{}' failed: {:?}", name, e),
}
})
});
match job_runner {
Ok(j) => {
self.scheduler.add(j).await?;
}
Err(e) => {
eprintln!("⚠️ [SCHEDULER] Failed to create job structure for '{}': {:?}", name_err, e);
}
}
}
self.scheduler.start().await?;
println!("⏰ [SCHEDULER] Job Scheduler Daemon started successfully.");
Ok(())
}
}