1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(
6 feature = "docsrs",
7 cfg_attr(doc, doc = ::document_features::document_features!())
8)]
9use 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
29pub type PgTask<Args = Vec<u8>> = Task<Args>;
31pub 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 .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 .and_then(|text: String| async move {
153 let confidence = 0.85; 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 .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 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}