Skip to main content

apalis_postgres/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3//!
4//! # Feature flags
5#![cfg_attr(
6    feature = "docsrs",
7    cfg_attr(doc, doc = ::document_features::document_features!())
8)]
9//!
10//! [`PostgresStorageWithListener`]: crate::PostgresStorage
11//! [`PostgresStorageFactory`]: crate::factory::PostgresStorageFactory
12
13use apalis_core::task::{Task, task_id::TaskId};
14mod backend;
15mod config;
16mod error;
17pub mod factory;
18mod from_row;
19mod persistence;
20mod pubsub;
21pub mod queries;
22mod sink;
23mod timestamp;
24
25pub use config::Config;
26pub use error::Error;
27pub use pubsub::{InsertEvent, Pubsub};
28
29/// An alias for [Task], specialized for Postgres.
30pub type PgTask<Args = Vec<u8>> = Task<Args>;
31/// An alias for [TaskId] using [TaskId::Ulid], specialized for Postgres.
32pub type PgTaskId = TaskId;
33pub use crate::backend::PostgresStorage;
34pub use sqlx::{PgPool, postgres::PgConnectOptions, postgres::PgConnection, postgres::PgListener};
35
36#[cfg(test)]
37mod tests {
38    use std::{collections::HashMap, env, time::Duration};
39
40    use apalis_workflow::SteppedFlow;
41    use futures::{StreamExt, stream};
42    use serde::{Deserialize, Serialize};
43    use sqlx::PgPool;
44
45    use crate::config::Config;
46    use apalis::prelude::*;
47
48    use super::*;
49
50    #[tokio::test]
51    async fn basic_worker() {
52        use apalis_core::backend::TaskSink;
53        let pool = PgPool::connect(env::var("DATABASE_URL").unwrap().as_str())
54            .await
55            .unwrap();
56        let config = Config::default()
57            .queue("sample")
58            .batch_size(50)
59            .lock_tasks(false);
60        let mut backend = PostgresStorage::new(&pool).with_config(config);
61
62        let mut items = stream::repeat_with(HashMap::default).take(1);
63        backend.push_stream(&mut items).await.unwrap();
64
65        async fn send_reminder(
66            _: HashMap<String, String>,
67            wrk: WorkerContext,
68        ) -> Result<(), BoxDynError> {
69            tokio::time::sleep(Duration::from_secs(2)).await;
70            wrk.stop().unwrap();
71            Ok(())
72        }
73
74        let worker = WorkerBuilder::new("rango-tango-1")
75            .backend(backend)
76            .build(send_reminder);
77        worker.run().await.unwrap();
78    }
79    #[tokio::test]
80    async fn notify_worker() {
81        let pool = PgPool::connect(env::var("DATABASE_URL").unwrap().as_str())
82            .await
83            .unwrap();
84        let config = Config::default()
85            .queue("test")
86            .persist_results(true)
87            .lock_tasks(false)
88            .batch_size(20);
89        let backend = PostgresStorage::new(&pool)
90            .with_config(config)
91            .with_pubsub();
92
93        let mut b = backend.clone();
94
95        tokio::spawn(async move {
96            tokio::time::sleep(Duration::from_secs(3)).await;
97            let task = TaskBuilder::new(42u32).priority(1).build();
98            b.push_task(task).await.unwrap();
99        });
100
101        async fn send_reminder(_: u32, wrk: WorkerContext) -> Result<(), BoxDynError> {
102            wrk.stop().unwrap();
103            Ok(())
104        }
105
106        let ctx = WorkerContext::new("rango-tango-2");
107        let worker = WorkerBuilder::new(&ctx)
108            .backend(backend)
109            .build(send_reminder);
110        worker.run().await.unwrap();
111        let run_for = ctx.elapsed();
112        assert!(
113            run_for < Duration::from_secs(4),
114            "Worker did not use notify mechanism"
115        );
116    }
117
118    #[tokio::test]
119    async fn test_workflow_complete() {
120        #[derive(Debug, Serialize, Deserialize, Clone)]
121        struct PipelineConfig {
122            min_confidence: f32,
123            enable_sentiment: bool,
124        }
125
126        #[derive(Debug, Serialize, Deserialize)]
127        struct UserInput {
128            text: String,
129        }
130
131        #[derive(Debug, Serialize, Deserialize)]
132        struct Classified {
133            text: String,
134            label: String,
135            confidence: f32,
136        }
137
138        #[derive(Debug, Serialize, Deserialize)]
139        struct Summary {
140            text: String,
141            sentiment: Option<String>,
142        }
143
144        let workflow = SteppedFlow::new("text-pipeline")
145            // Step 1: Preprocess input (e.g., tokenize, lowercase)
146            .and_then(|input: UserInput, worker: WorkerContext| async move {
147                worker.emit(format!("Preprocessing input: {}", input.text));
148                let processed = input.text.to_lowercase();
149                Ok::<_, BoxDynError>(processed)
150            })
151            // Step 2: Classify text
152            .and_then(|text: String| async move {
153                let confidence = 0.85; // pretend model confidence
154                let items = text.split_whitespace().collect::<Vec<_>>();
155                let results = items
156                    .into_iter()
157                    .map(|x| Classified {
158                        text: x.to_string(),
159                        label: if x.contains("rust") {
160                            "Tech"
161                        } else {
162                            "General"
163                        }
164                        .to_string(),
165                        confidence,
166                    })
167                    .collect::<Vec<_>>();
168                Ok::<_, BoxDynError>(results)
169            })
170            // Step 3: Filter out low-confidence predictions
171            .filter_map(
172                |c: Classified| async move { if c.confidence >= 0.6 { Some(c) } else { None } },
173            )
174            .filter_map(move |c: Classified, config: Data<PipelineConfig>| {
175                let cfg = config.enable_sentiment;
176                async move {
177                    if !cfg {
178                        return Some(Summary {
179                            text: c.text,
180                            sentiment: None,
181                        });
182                    }
183
184                    // pretend we run a sentiment model
185                    let sentiment = if c.text.contains("delightful") {
186                        "positive"
187                    } else {
188                        "neutral"
189                    };
190                    Some(Summary {
191                        text: c.text,
192                        sentiment: Some(sentiment.to_string()),
193                    })
194                }
195            })
196            .and_then(|a: Vec<Summary>, worker: WorkerContext| async move {
197                dbg!(&a);
198                worker.emit(format!("Generated {} summaries", a.len()));
199                worker.stop()
200            });
201
202        let pool = PgPool::connect(env::var("DATABASE_URL").unwrap().as_str())
203            .await
204            .unwrap();
205        let config = Config::default().queue("test");
206        let mut backend = PostgresStorage::new(&pool)
207            .with_config(config)
208            .with_pubsub();
209
210        let input = UserInput {
211            text: "Rust makes systems programming delightful!".to_string(),
212        };
213        backend.push(input).await.unwrap();
214
215        let worker = WorkerBuilder::new("rango-tango")
216            .backend(backend)
217            .data(PipelineConfig {
218                min_confidence: 0.8,
219                enable_sentiment: true,
220            })
221            .on_event(|ctx, ev| match ev {
222                Event::Custom(msg) => {
223                    if let Some(m) = msg.downcast_ref::<String>() {
224                        println!("Custom Message: {m}");
225                    }
226                }
227                Event::Error(_) => {
228                    println!("On Error = {ev:?}");
229                    ctx.stop().unwrap();
230                }
231                _ => {
232                    println!("On Event = {ev:?}");
233                }
234            })
235            .build(workflow);
236        worker.run().await.unwrap();
237    }
238}