use std::{
marker::PhantomData,
task::{Context, Poll},
};
use apalis_codec::json::JsonCodec;
use apalis_core::{
backend::{
Backend, BackendConfig, WireFormatBackend,
ext::poll_strategy::{PollWith, StreamStrategy},
finalize::Durable,
persistence::{Persisted, TaskPersistLayer},
},
features_table,
worker::context::WorkerContext,
};
use serde_json::Value;
use sqlx::PgPool;
use ulid::Ulid;
use crate::{PgTask, config::Config, error::Error, persistence::SqlxPersistence, pubsub::Pubsub};
#[doc = features_table! {
setup = r#"
# {
# use apalis_postgres::PostgresStorage;
# use sqlx::PgPool;
# let pool = PgPool::connect(std::env::var("DATABASE_URL").unwrap().as_str()).await.unwrap();
# PostgresStorage::setup(&pool).await.unwrap();
# PostgresStorage::<u32>::new(&pool)
# };
"#,
Backend => supported("Supports storage and retrieval of tasks", true),
TaskSink => supported("Ability to push new tasks", true),
Serialization => supported("Serialization support for arguments", true),
Workflow => supported("Flexible enough to support workflows", true),
WebUI => supported("Expose a web interface for monitoring tasks", true),
FetchById => supported("Allow fetching a task by its ID", false),
RegisterWorker => supported("Allow registering a worker with the backend", false),
MakeShared => supported("Share one connection across multiple workers via [`PostgresStorageFactory`]", false),
WaitForCompletion => supported("Wait for tasks to complete without blocking", true),
ResumeById => supported("Resume a task by its ID", false),
ResumeAbandoned => supported("Resume abandoned tasks", false),
ListWorkers => supported("List all workers registered with the backend", false),
ListTasks => supported("List all tasks in the backend", false),
}]
#[pin_project::pin_project]
pub struct PostgresStorage<Args> {
#[pin]
pub(crate) persistence: Persisted<SqlxPersistence>,
codec: JsonCodec,
_marker: PhantomData<Args>,
}
impl<Args> Clone for PostgresStorage<Args> {
fn clone(&self) -> Self {
Self {
persistence: self.persistence.clone(),
codec: self.codec.clone(),
_marker: PhantomData,
}
}
}
impl PostgresStorage<()> {
#[cfg(feature = "migrate")]
pub async fn setup(pool: &PgPool) -> Result<(), Error> {
Self::migrations()
.run(pool)
.await
.map_err(sqlx::Error::from)?;
Ok(())
}
#[cfg(feature = "migrate")]
pub fn migrations() -> sqlx::migrate::Migrator {
sqlx::migrate!("./migrations")
}
}
impl<Args> PostgresStorage<Args> {
pub fn new(pool: &PgPool) -> Self {
let config = Config::default().queue(std::any::type_name::<Args>());
let persistence = Persisted::new(SqlxPersistence {
config,
pool: pool.clone(),
});
Self {
_marker: PhantomData,
codec: JsonCodec::default(),
persistence,
}
}
pub fn with_pubsub(self) -> PollWith<Self, StreamStrategy<Pubsub>> {
let pool = self.pool().clone();
let config = self.config();
let namespace = config.queue.to_string();
PollWith::new(self, StreamStrategy::new(Pubsub::new(pool, namespace)))
}
pub fn with_config(mut self, config: Config) -> Self {
self.persistence.config = config;
self
}
pub fn pool(&self) -> &PgPool {
&self.persistence.pool
}
pub fn config(&self) -> &Config {
&self.persistence.config
}
}
impl<Args> Backend for PostgresStorage<Args> {
type Task = PgTask;
type Error = Error;
fn poll_ready(
&mut self,
cx: &mut Context<'_>,
worker: &WorkerContext,
) -> Poll<Result<(), Self::Error>> {
self.persistence
.poll_ready(cx, worker, self.config().heartbeat_interval)
}
fn poll_next(
&mut self,
cx: &mut Context<'_>,
worker: &WorkerContext,
) -> Poll<Option<Result<PgTask, Self::Error>>> {
self.persistence.poll_next(cx, worker)
}
fn poll_close(
&mut self,
cx: &mut Context<'_>,
worker: &WorkerContext,
) -> Poll<Result<(), Self::Error>> {
self.persistence.poll_close(cx, worker)
}
}
impl<Args> BackendConfig for PostgresStorage<Args> {
type Args = Args;
type Kind = Durable;
type Id = Ulid;
type Config = Config;
type Layer = TaskPersistLayer<JsonCodec<Value>, Value>;
fn config(&self) -> &Self::Config {
&self.persistence.config
}
fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer {
self.persistence
.layer(JsonCodec::<Value>::default(), self.config().batch_size)
.persist_results(self.config().persist_results)
.lock_tasks(self.config().lock_tasks)
}
}
impl<Args> WireFormatBackend for PostgresStorage<Args> {
type Codec = JsonCodec;
type Compact = Vec<u8>;
fn codec(&self) -> &Self::Codec {
&self.codec
}
}