mod embeddings;
mod jobs;
mod materialized_views;
pub(crate) mod signing;
mod types;
pub use jobs::validate_job_target;
pub use types::{Job, JobStatus};
use crate::scripting::{ScriptEngine, ScriptStats};
use crate::storage::StorageEngine;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
pub struct QueueWorker {
pub(crate) storage: Arc<StorageEngine>,
pub(crate) script_engine: Arc<ScriptEngine>,
pub(crate) http_client: reqwest::Client,
pub(crate) dev_http_client: reqwest::Client,
notifier: broadcast::Sender<()>,
pub(crate) claiming_lock: tokio::sync::Mutex<()>,
pub(crate) mv_next_due: std::sync::Mutex<std::collections::HashMap<String, u64>>,
pub(crate) job_permits: Arc<tokio::sync::Semaphore>,
pub(crate) in_flight: Arc<std::sync::Mutex<std::collections::HashSet<(String, String)>>>,
last_job_sweep: std::sync::Mutex<Option<std::time::Instant>>,
pub(crate) embed_backoff: std::sync::Mutex<std::collections::HashMap<String, u64>>,
}
const JOB_SWEEP_INTERVAL: Duration = Duration::from_secs(60);
fn queue_max_concurrency() -> usize {
std::env::var("SOLIDB_QUEUE_MAX_CONCURRENCY")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.saturating_mul(4)
})
.min(tokio::sync::Semaphore::MAX_PERMITS)
}
impl QueueWorker {
pub fn new(storage: Arc<StorageEngine>, stats: Arc<ScriptStats>) -> Self {
let (notifier, _) = broadcast::channel(100);
let script_engine = Arc::new(
ScriptEngine::new(storage.clone(), stats).with_queue_notifier(notifier.clone()),
);
let http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("reqwest::Client builds with defaults");
let dev_http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.redirect(reqwest::redirect::Policy::none())
.danger_accept_invalid_certs(true)
.build()
.expect("reqwest::Client builds with permissive TLS");
Self {
storage,
script_engine,
http_client,
dev_http_client,
notifier,
claiming_lock: tokio::sync::Mutex::new(()),
mv_next_due: std::sync::Mutex::new(std::collections::HashMap::new()),
job_permits: Arc::new(tokio::sync::Semaphore::new(queue_max_concurrency())),
in_flight: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
last_job_sweep: std::sync::Mutex::new(None),
embed_backoff: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
pub fn notifier(&self) -> broadcast::Sender<()> {
self.notifier.clone()
}
async fn maybe_sweep_jobs(&self) {
{
let mut last = match self.last_job_sweep.lock() {
Ok(l) => l,
Err(_) => return,
};
if last.is_some_and(|t| t.elapsed() < JOB_SWEEP_INTERVAL) {
return;
}
*last = Some(std::time::Instant::now());
}
self.sweep_jobs().await;
}
pub async fn start(self: Arc<Self>) {
tracing::info!("Starting QueueWorker");
let mut rx = self.notifier.subscribe();
loop {
tokio::select! {
_ = rx.recv() => {
tracing::debug!("Queue worker woke up by notification");
}
_ = tokio::time::sleep(Duration::from_secs(5)) => {
tracing::debug!("Queue worker periodic check");
}
}
self.maybe_sweep_jobs().await;
self.check_jobs().await;
self.check_embeddings().await;
self.check_materialized_views().await;
}
}
}