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().run(pool).await.map_err(|r| {
107 use sqlx::migrate::MigrateError::VersionMismatch;
108
109 if let VersionMismatch(20220530084123) = r {
110 Error::UpgradeRequired {
111 current: "0.7.4".into(),
112 required: "1.0.0".into(),
113 link: Some(
114 "https://github.com/apalis-dev/apalis-postgres#upgrading-to-10".into(),
115 ),
116 }
117 } else {
118 Error::Database(r.into())
119 }
120 })?;
121 Ok(())
122 }
123
124 #[cfg(feature = "migrate")]
126 pub fn migrations() -> sqlx::migrate::Migrator {
127 sqlx::migrate!("./migrations")
128 }
129}
130
131impl<Args> PostgresStorage<Args> {
132 pub fn new(pool: &PgPool) -> Self {
134 let config = Config::default().queue(std::any::type_name::<Args>());
135 let persistence = Persisted::new(SqlxPersistence {
136 config,
137 pool: pool.clone(),
138 });
139 Self {
140 _marker: PhantomData,
141 codec: JsonCodec::default(),
142 persistence,
143 }
144 }
145
146 pub fn with_pubsub(self) -> PollWith<Self, StreamStrategy<Pubsub>> {
148 let pool = self.pool().clone();
149 let config = self.config();
150 let namespace = config.queue.to_string();
151 PollWith::new(self, StreamStrategy::new(Pubsub::new(pool, namespace)))
152 }
153
154 pub fn with_config(mut self, config: Config) -> Self {
156 self.persistence.config = config;
157 self
158 }
159
160 pub fn pool(&self) -> &PgPool {
162 &self.persistence.pool
163 }
164
165 pub fn config(&self) -> &Config {
167 &self.persistence.config
168 }
169}
170
171impl<Args> Backend for PostgresStorage<Args> {
172 type Task = PgTask;
173 type Error = Error;
174
175 fn poll_ready(
176 &mut self,
177 cx: &mut Context<'_>,
178 worker: &WorkerContext,
179 ) -> Poll<Result<(), Self::Error>> {
180 self.persistence
181 .poll_ready(cx, worker, self.config().heartbeat_interval)
182 }
183
184 fn poll_next(
185 &mut self,
186 cx: &mut Context<'_>,
187 worker: &WorkerContext,
188 ) -> Poll<Option<Result<PgTask, Self::Error>>> {
189 self.persistence.poll_next(cx, worker)
190 }
191
192 fn poll_close(
193 &mut self,
194 cx: &mut Context<'_>,
195 worker: &WorkerContext,
196 ) -> Poll<Result<(), Self::Error>> {
197 self.persistence.poll_close(cx, worker)
198 }
199}
200
201impl<Args> BackendConfig for PostgresStorage<Args> {
202 type Args = Args;
203
204 type Kind = Durable;
205
206 type Id = Ulid;
207
208 type Config = Config;
209
210 type Layer = TaskPersistLayer<JsonCodec<Value>, Value>;
211
212 fn config(&self) -> &Self::Config {
213 &self.persistence.config
214 }
215
216 fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer {
217 self.persistence
218 .layer(JsonCodec::<Value>::default(), self.config().batch_size)
219 .persist_results(self.config().persist_results)
220 .lock_tasks(self.config().lock_tasks)
221 }
222}
223
224impl<Args> WireFormatBackend for PostgresStorage<Args> {
225 type Codec = JsonCodec;
226
227 type Compact = Vec<u8>;
228
229 fn codec(&self) -> &Self::Codec {
230 &self.codec
231 }
232}