use std::{
cmp::max,
collections::{HashMap, HashSet},
future::ready,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use crate::{
Config, JOBS_TABLE, SqliteStorage, SqliteTask,
callback::{DbEvent, update_hook_callback},
};
use crate::{Error, from_row::SqliteTaskRow};
use apalis_core::backend::{BackendConfig, ext::interleave::Interleave, factory::BackendFactory};
use futures::{
FutureExt, SinkExt, Stream, StreamExt, TryStreamExt,
channel::mpsc::{self, Receiver, Sender},
future::{BoxFuture, Shared},
lock::Mutex,
ready,
};
use serde::{Serialize, de::DeserializeOwned};
use sqlx::{Sqlite, SqlitePool, pool::PoolOptions, sqlite::SqliteOperation};
pub type SharedSqliteStorage<Args> = Interleave<SqliteStorage<Args>, SharedFetcher>;
type Registry = Arc<Mutex<HashMap<String, Sender<Result<SqliteTask, Error>>>>>;
#[derive(Clone, Debug)]
pub struct SqliteStorageFactory {
pool: SqlitePool,
registry: Registry,
drive: Shared<BoxFuture<'static, ()>>,
}
impl SqliteStorageFactory {
#[must_use]
pub fn pool(&self) -> &SqlitePool {
&self.pool
}
}
impl SqliteStorageFactory {
#[must_use]
pub fn new(url: &str) -> SqliteStorageFactory {
Self::new_with_pool_options(
url,
PoolOptions::new().max_lifetime(None).idle_timeout(None),
)
}
#[must_use]
pub fn new_with_pool_options(url: &str, options: PoolOptions<Sqlite>) -> SqliteStorageFactory {
let (tx, rx) = mpsc::unbounded::<DbEvent>();
let pool = options
.after_connect(move |conn, _meta| {
let mut tx = tx.clone();
Box::pin(async move {
let mut lock_handle = conn.lock_handle().await?;
lock_handle.set_update_hook(move |ev| update_hook_callback(ev, &mut tx));
Ok(())
})
})
.connect_lazy(url)
.expect("Failed to create Sqlite pool");
let registry: Registry = Registry::default();
let p = pool.clone();
let instances = registry.clone();
SqliteStorageFactory {
pool,
drive: async move {
rx.filter(|a| {
ready(a.operation() == &SqliteOperation::Insert && a.table_name() == JOBS_TABLE)
})
.ready_chunks(instances.try_lock().map(|r| r.len()).unwrap_or(10))
.then(|events| {
let row_ids = events.iter().map(|e| e.rowid()).collect::<HashSet<i64>>();
let instances = instances.clone();
let pool = p.clone();
async move {
let instances = instances.lock().await;
let job_types = serde_json::to_string(
&instances.keys().cloned().collect::<Vec<String>>(),
)
.map_err(Error::JsonError)?;
let row_ids = serde_json::to_string(&row_ids).map_err(Error::JsonError)?;
let mut tx = pool.begin().await?;
let batch_size = max(10, instances.len()) as i32;
let res: Vec<_> = sqlx::query_file_as!(
SqliteTaskRow,
"queries/backend/fetch_next_shared.sql",
job_types,
row_ids,
batch_size,
)
.fetch(&mut *tx)
.map_ok(|r| r.try_into())
.try_collect()
.await?;
tx.commit().await?;
Ok::<_, Error>(res)
}
})
.map_ok(futures::stream::iter)
.try_flatten()
.for_each(|r: Result<SqliteTask, Error>| async {
match r {
Ok(task) => {
let mut instances = instances.lock().await;
if let Some(tx) = instances
.get_mut(&task.queue().expect("Queue must be set").to_string())
&& let Err(e) = tx.send(Ok(task)).await
{
log::error!("Error pushing task: {e:?}");
}
}
Err(e) => {
log::error!("Error fetching tasks: {e:?}");
}
}
})
.await;
}
.boxed()
.shared(),
registry,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SharedSqliteError {
#[error("Namespace {0} already exists")]
NamespaceExists(String),
#[error("Could not acquire registry lock")]
RegistryLocked,
}
impl<Args> BackendFactory<Args> for SqliteStorageFactory
where
Args: Send + Sync + Serialize + DeserializeOwned + 'static,
{
type Backend = SharedSqliteStorage<Args>;
type Error = SharedSqliteError;
fn create(&mut self) -> Result<Self::Backend, Self::Error>
where
<Self::Backend as BackendConfig>::Config: Default,
{
let config = Config::default().queue(std::any::type_name::<Args>());
self.create_with_config(config)
}
fn create_with_config(
&mut self,
config: <Self::Backend as BackendConfig>::Config,
) -> Result<Self::Backend, Self::Error> {
let (tx, rx) = mpsc::channel(config.batch_size);
let mut r = self
.registry
.try_lock()
.ok_or(SharedSqliteError::RegistryLocked)?;
if r.insert(config.queue.to_string(), tx).is_some() {
return Err(SharedSqliteError::NamespaceExists(config.queue.to_string()));
}
Ok(Interleave::new(
SqliteStorage::new(&self.pool).with_config(config),
SharedFetcher {
poller: self.drive.clone(),
receiver: Arc::new(Mutex::new(rx)),
},
))
}
}
#[derive(Clone, Debug)]
pub struct SharedFetcher {
poller: Shared<BoxFuture<'static, ()>>,
receiver: Arc<Mutex<Receiver<Result<SqliteTask, Error>>>>,
}
impl Stream for SharedFetcher {
type Item = Result<SqliteTask, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let _ = this.poller.poll_unpin(cx);
let mut guard = ready!(this.receiver.lock().poll_unpin(cx));
guard.poll_next_unpin(cx)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use apalis_core::{
backend::TaskSink,
error::BoxDynError,
task::task_id::TaskId,
worker::{builder::WorkerBuilder, context::WorkerContext},
};
use super::*;
#[tokio::test]
async fn factory_worker() {
let mut factory = SqliteStorageFactory::new(":memory:");
SqliteStorage::setup(factory.pool()).await.unwrap();
let mut map_store = factory.create().unwrap();
let mut int_store: SharedSqliteStorage<usize> = factory.create().unwrap();
map_store
.push(HashMap::<String, i32>::from([("value".to_string(), 42)]))
.await
.unwrap();
int_store.push(99).await.unwrap();
async fn send_reminder<T>(
_: T,
_task_id: TaskId,
wrk: WorkerContext,
) -> Result<(), BoxDynError> {
tokio::time::sleep(Duration::from_secs(2)).await;
wrk.stop().unwrap();
Ok(())
}
let int_worker = WorkerBuilder::new("rango-tango-2")
.backend(int_store)
.build(send_reminder);
let map_worker = WorkerBuilder::new("rango-tango-1")
.backend(map_store)
.build(send_reminder);
tokio::try_join!(int_worker.run(), map_worker.run()).unwrap();
}
}