apalis_postgres/
backend.rs1use 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#[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#[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 #[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 #[cfg(feature = "migrate")]
115 pub fn migrations() -> sqlx::migrate::Migrator {
116 sqlx::migrate!("./migrations")
117 }
118}
119
120impl<Args> PostgresStorage<Args> {
121 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 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 pub fn with_config(mut self, config: Config) -> Self {
145 self.persistence.config = config;
146 self
147 }
148
149 pub fn pool(&self) -> &PgPool {
151 &self.persistence.pool
152 }
153
154 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}