Skip to main content

apalis_postgres/
backend.rs

1use std::{
2    marker::PhantomData,
3    task::{Context, Poll},
4};
5
6use apalis_codec::json::JsonCodec;
7use apalis_core::{
8    backend::{
9        Backend, BackendConfig, WireFormatBackend,
10        ext::poll_strategy::{PollWith, StreamStrategy},
11        finalize::Durable,
12        persistence::{Persisted, TaskPersistLayer},
13    },
14    features_table,
15    worker::context::WorkerContext,
16};
17use serde_json::Value;
18use sqlx::PgPool;
19use ulid::Ulid;
20
21use crate::{PgTask, config::Config, error::Error, persistence::SqlxPersistence, pubsub::Pubsub};
22
23/// A backend for persisting and consuming jobs behind a postgres database
24#[doc = features_table! {
25    setup = r#"
26        # {
27        #   use apalis_postgres::PostgresStorage;
28        #   use sqlx::PgPool;
29        #   let pool = PgPool::connect(std::env::var("DATABASE_URL").unwrap().as_str()).await.unwrap();
30        #   PostgresStorage::setup(&pool).await.unwrap();
31        #   PostgresStorage::<u32>::new(&pool)
32        # };
33    "#,
34
35    Backend => supported("Supports storage and retrieval of tasks", true),
36    TaskSink => supported("Ability to push new tasks", true),
37    Serialization => supported("Serialization support for arguments", true),
38    Workflow => supported("Flexible enough to support workflows", true),
39    WebUI => supported("Expose a web interface for monitoring tasks", true),
40    FetchById => supported("Allow fetching a task by its ID", false),
41    RegisterWorker => supported("Allow registering a worker with the backend", false),
42    MakeShared => supported("Share one connection across multiple workers via [`PostgresStorageFactory`]", false),
43    WaitForCompletion => supported("Wait for tasks to complete without blocking", true),
44    ResumeById => supported("Resume a task by its ID", false),
45    ResumeAbandoned => supported("Resume abandoned tasks", false),
46    ListWorkers => supported("List all workers registered with the backend", false),
47    ListTasks => supported("List all tasks in the backend", false),
48}]
49///
50/// [`PostgresStorageFactory`]: crate::factory::PostgresStorageFactory
51#[pin_project::pin_project]
52pub struct PostgresStorage<Args> {
53    #[pin]
54    pub(crate) persistence: Persisted<SqlxPersistence>,
55    codec: JsonCodec,
56    _marker: PhantomData<Args>,
57}
58
59impl<Args> Clone for PostgresStorage<Args> {
60    fn clone(&self) -> Self {
61        Self {
62            persistence: self.persistence.clone(),
63            codec: self.codec.clone(),
64            _marker: PhantomData,
65        }
66    }
67}
68
69impl PostgresStorage<()> {
70    /// Runs the PostgreSQL storage migrations.
71    ///
72    /// ## Fresh databases
73    ///
74    /// No manual setup is required. Calling `setup()` will create the required
75    /// tables and migration history.
76    ///
77    /// ## Upgrading to `1.0`
78    ///
79    /// > **⚠️ Important:** Existing databases created by a pre-`1.0` version
80    /// > require a **one-time manual migration** before calling `setup()`.
81    ///
82    /// The `1.0` migration history is no longer relocated automatically by
83    /// `setup()`. Follow the **"Upgrading to 1.0"** section in the README to
84    /// perform the required transition.
85    ///
86    /// After the transition has been completed, `setup()` can be used normally
87    /// for subsequent migrations.
88    ///
89    /// ## Example
90    ///
91    /// ```no_run
92    /// use apalis_postgres::PostgresStorage;
93    /// use sqlx::PgPool;
94    ///
95    /// # async fn run(pool: PgPool) -> Result<(), apalis_postgres::Error> {
96    /// PostgresStorage::<()>::setup(&pool).await?;
97    /// # Ok(())
98    /// # }
99    /// ```
100    ///
101    /// ## Errors
102    ///
103    /// Returns an error if the migrations cannot be applied to the database.
104    #[cfg(feature = "migrate")]
105    pub async fn setup(pool: &PgPool) -> Result<(), Error> {
106        Self::migrations()
107            .run(pool)
108            .await
109            .map_err(sqlx::Error::from)?;
110        Ok(())
111    }
112
113    /// Get postgres migrations without running them
114    #[cfg(feature = "migrate")]
115    pub fn migrations() -> sqlx::migrate::Migrator {
116        sqlx::migrate!("./migrations")
117    }
118}
119
120impl<Args> PostgresStorage<Args> {
121    /// Creates a new PostgresStorage instance.
122    pub fn new(pool: &PgPool) -> Self {
123        let config = Config::default().queue(std::any::type_name::<Args>());
124        let persistence = Persisted::new(SqlxPersistence {
125            config,
126            pool: pool.clone(),
127        });
128        Self {
129            _marker: PhantomData,
130            codec: JsonCodec::default(),
131            persistence,
132        }
133    }
134
135    /// Mount a standalone [`Pubsub`] which uses its own connection under the hood
136    pub fn with_pubsub(self) -> PollWith<Self, StreamStrategy<Pubsub>> {
137        let pool = self.pool().clone();
138        let config = self.config();
139        let namespace = config.queue.to_string();
140        PollWith::new(self, StreamStrategy::new(Pubsub::new(pool, namespace)))
141    }
142
143    /// Configure a new PostgresStorage instance.
144    pub fn with_config(mut self, config: Config) -> Self {
145        self.persistence.config = config;
146        self
147    }
148
149    /// Returns a reference to the pool.
150    pub fn pool(&self) -> &PgPool {
151        &self.persistence.pool
152    }
153
154    /// Returns a reference to the config.
155    pub fn config(&self) -> &Config {
156        &self.persistence.config
157    }
158}
159
160impl<Args> Backend for PostgresStorage<Args> {
161    type Task = PgTask;
162    type Error = Error;
163
164    fn poll_ready(
165        &mut self,
166        cx: &mut Context<'_>,
167        worker: &WorkerContext,
168    ) -> Poll<Result<(), Self::Error>> {
169        self.persistence
170            .poll_ready(cx, worker, self.config().heartbeat_interval)
171    }
172
173    fn poll_next(
174        &mut self,
175        cx: &mut Context<'_>,
176        worker: &WorkerContext,
177    ) -> Poll<Option<Result<PgTask, Self::Error>>> {
178        self.persistence.poll_next(cx, worker)
179    }
180
181    fn poll_close(
182        &mut self,
183        cx: &mut Context<'_>,
184        worker: &WorkerContext,
185    ) -> Poll<Result<(), Self::Error>> {
186        self.persistence.poll_close(cx, worker)
187    }
188}
189
190impl<Args> BackendConfig for PostgresStorage<Args> {
191    type Args = Args;
192
193    type Kind = Durable;
194
195    type Id = Ulid;
196
197    type Config = Config;
198
199    type Layer = TaskPersistLayer<JsonCodec<Value>, Value>;
200
201    fn config(&self) -> &Self::Config {
202        &self.persistence.config
203    }
204
205    fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer {
206        self.persistence
207            .layer(JsonCodec::<Value>::default(), self.config().batch_size)
208            .persist_results(self.config().persist_results)
209            .lock_tasks(self.config().lock_tasks)
210    }
211}
212
213impl<Args> WireFormatBackend for PostgresStorage<Args> {
214    type Codec = JsonCodec;
215
216    type Compact = Vec<u8>;
217
218    fn codec(&self) -> &Self::Codec {
219        &self.codec
220    }
221}