1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3use apalis_codec::json::JsonCodec;
4use futures_core::{Stream, stream::BoxStream};
5use futures_util::{
6 StreamExt,
7 future::{Ready, ready},
8};
9use serde::{Deserialize, Serialize, de::DeserializeOwned};
10use serde_json::Value;
11use std::{
12 collections::{BTreeMap, HashMap},
13 fmt::Debug,
14 fs::{File, OpenOptions},
15 io::{self, BufRead, Seek, SeekFrom, Write},
16 marker::PhantomData,
17 num::ParseIntError,
18 path::{Path, PathBuf},
19 pin::Pin,
20 str::FromStr,
21 sync::{Arc, Mutex, RwLock},
22 task::{Context, Poll},
23 time::Duration,
24};
25use tempfile::NamedTempFile;
26
27use crate::error::FileStorageError;
28
29use self::util::RawTask;
30use apalis_core::{
31 backend::{
32 Backend, BackendConfig, TaskResult, TryNewBackend, WaitForCompletion, WireFormatBackend,
33 finalize::Durable,
34 },
35 error::BoxDynError,
36 features_table,
37 task::{
38 ExecutionContext, Task,
39 builder::TaskBuilder,
40 metadata::MetadataStore,
41 status::Status,
42 task_id::{RandomId, TaskId},
43 },
44 worker::{
45 context::WorkerContext,
46 ext::ack::{Acknowledge, AcknowledgeLayer},
47 },
48};
49use std::io::BufReader;
50
51use fd_lock::RwLock as FileLock;
52
53mod adapter;
54mod error;
55mod shared;
56mod sink;
57mod util;
58
59pub use self::shared::SharedJsonStore;
60pub use adapter::Adapter;
61
62#[derive(Debug, Clone)]
64#[non_exhaustive]
65pub enum SyncPolicy {
66 Instant,
68 Periodic(Duration),
70 Manual,
72}
73
74#[derive(Debug)]
75enum PendingChange {
76 Append(Vec<u8>),
78 RewriteLine {
80 line_id: usize,
81 new_bytes: Vec<u8>,
83 },
84}
85
86#[doc = features_table! {
101 setup = r#"
102 # {
103 # use apalis_file_storage::JsonStorage;;
104 # let mut backend: JsonStorage<u32> = JsonStorage::new_temp().unwrap();
105 # backend
106 # };
107 "#,
108 Backend => supported("Basic Backend functionality", true),
109 TaskSink => supported("Ability to push new tasks", true),
110 Serialization => limited("Serialization support for arguments. Only accepts `json`", false),
111 WebUI => not_implemented("Expose a web interface for monitoring tasks"),
112 FetchById => not_implemented("Allow fetching a task by its ID"),
113 RegisterWorker => not_supported("Allow registering a worker with the backend"),
114 "[`PipeExt`]" => supported("Allow other backends to pipe to this backend", false),
115 BackendFactory => supported("Share the same storage across multiple workers via [`SharedJsonStore`]", false),
116 Workflow => supported("Flexible enough to support workflows", true),
117 WaitForCompletion => supported("Wait for tasks to complete without blocking", true),
118 ResumeById => not_implemented("Resume a task by its ID"),
119 ResumeAbandoned => not_implemented("Resume abandoned tasks"),
120 ListWorkers => not_supported("List all workers registered with the backend"),
121 ListTasks => not_implemented("List all tasks in the backend"),
122}]
123#[derive(Debug)]
124pub struct FileStorage<Args, A: Adapter> {
125 adapter: A,
126 lock: Arc<RwLock<FileLock<File>>>,
127 path: PathBuf,
128 entries: Arc<RwLock<Vec<RawTask>>>,
130 pending: Arc<RwLock<Vec<PendingChange>>>,
132 read_cursor: usize,
134 sync_policy: SyncPolicy,
135 last_flush: std::time::Instant,
136 codec: JsonCodec<Value>,
137 _args: PhantomData<Args>,
138}
139
140impl<Args, A> Clone for FileStorage<Args, A>
141where
142 A: Adapter + Clone,
143{
144 fn clone(&self) -> Self {
145 Self {
146 adapter: self.adapter.clone(),
147 lock: Arc::clone(&self.lock),
148 path: self.path.clone(),
149 entries: Arc::clone(&self.entries),
150 pending: Arc::clone(&self.pending),
151 read_cursor: self.read_cursor,
152 sync_policy: self.sync_policy.clone(),
153 last_flush: self.last_flush,
154 codec: self.codec.clone(),
155 _args: PhantomData,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Copy, Default)]
162pub struct JsonAdapter;
163
164pub type JsonStorage<Args> = FileStorage<Args, JsonAdapter>;
166
167impl Adapter for JsonAdapter {
168 type Line = serde_json::Value;
169
170 type Error = serde_json::Error;
171
172 fn serialize(&self, line: &Self::Line) -> Result<Vec<u8>, Self::Error> {
173 let mut bytes = serde_json::to_vec(line)?;
174 bytes.push(b'\n');
175 Ok(bytes)
176 }
177
178 fn deserialize(&self, raw: &[u8]) -> Result<Self::Line, Self::Error> {
179 serde_json::from_slice(raw.trim_ascii())
180 }
181
182 fn to_entry(&self, line: Self::Line) -> Result<RawTask, Self::Error> {
183 serde_json::from_value(line)
184 }
185
186 fn from_entry(entry: &RawTask) -> Result<Self::Line, Self::Error> {
187 serde_json::to_value(entry)
188 }
189}
190
191impl<A: Adapter, Args> FileStorage<Args, A> {
192 pub fn new(path: impl AsRef<Path>) -> Result<Self, FileStorageError<A>>
194 where
195 A: Default,
196 {
197 let adapter = A::default();
198 let path = path.as_ref().to_path_buf();
199
200 let file = OpenOptions::new()
201 .create(true)
202 .read(true)
203 .write(true)
204 .truncate(true)
205 .open(&path)
206 .map_err(FileStorageError::Io)?;
207
208 let lock = Arc::new(RwLock::new(FileLock::new(file)));
209
210 let entries = {
211 let guard = lock.read().map_err(|e| {
212 FileStorageError::Io(std::io::Error::new(
213 std::io::ErrorKind::WouldBlock,
214 e.to_string(),
215 ))
216 })?;
217 let guard = guard
218 .try_read()
219 .map_err(|_| FileStorageError::WouldBlockLock)?;
220 Self::load_entries(&adapter, &guard)?
221 };
222
223 Ok(Self {
224 adapter,
225 lock,
226 path,
227 entries: Arc::new(RwLock::new(entries)),
228 pending: Default::default(),
229 read_cursor: 0,
230 sync_policy: SyncPolicy::Instant,
231 last_flush: std::time::Instant::now(),
232 codec: JsonCodec::default(),
233 _args: PhantomData,
234 })
235 }
236
237 pub fn new_temp() -> Result<Self, FileStorageError<A>>
239 where
240 A: Default,
241 {
242 let p = NamedTempFile::new()?.path().to_path_buf();
243 Self::new(p)
244 }
245
246 pub fn set_result(
248 &mut self,
249 line_id: usize,
250 result: serde_json::Value,
251 ) -> Result<(), FileStorageError<A>> {
252 {
253 let mut entries = self
254 .entries
255 .try_write()
256 .map_err(|_| FileStorageError::WouldBlockLock)?;
257 let entry = entries
258 .get_mut(line_id)
259 .ok_or(FileStorageError::JobNotFound { line_id })?;
260
261 entry.result = Some(result);
262
263 let line = A::from_entry(entry).map_err(FileStorageError::AdapterError)?;
265 let new_bytes = self
266 .adapter
267 .serialize(&line)
268 .map_err(FileStorageError::AdapterError)?;
269
270 self.pending
271 .try_write()
272 .map_err(|_| FileStorageError::WouldBlockLock)?
273 .push(PendingChange::RewriteLine { line_id, new_bytes });
274 }
275
276 if matches!(self.sync_policy, SyncPolicy::Instant) {
277 self.flush()?;
278 }
279 Ok(())
280 }
281
282 pub fn flush(&mut self) -> Result<(), FileStorageError<A>> {
284 if self
285 .pending
286 .try_read()
287 .map_err(|_| FileStorageError::WouldBlockLock)?
288 .is_empty()
289 {
290 return Ok(());
291 }
292
293 let mut outer_guard = self.lock.write().map_err(|e| {
294 FileStorageError::Io(std::io::Error::new(
295 std::io::ErrorKind::WouldBlock,
296 e.to_string(),
297 ))
298 })?;
299 let mut guard = outer_guard
300 .try_write()
301 .map_err(|_| FileStorageError::WouldBlockLock)?;
302
303 let has_rewrites = self
305 .pending
306 .try_read()
307 .map_err(|_| FileStorageError::WouldBlockLock)?
308 .iter()
309 .any(|c| matches!(c, PendingChange::RewriteLine { .. }));
310
311 if has_rewrites {
312 let mut rewrites: std::collections::HashMap<usize, Vec<u8>> =
318 std::collections::HashMap::new();
319 let mut extra_appends: Vec<Vec<u8>> = Vec::new();
320
321 for change in self
322 .pending
323 .try_write()
324 .map_err(|_| FileStorageError::WouldBlockLock)?
325 .drain(..)
326 {
327 match change {
328 PendingChange::RewriteLine { line_id, new_bytes } => {
329 rewrites.insert(line_id, new_bytes);
330 }
331 PendingChange::Append(bytes) => {
332 extra_appends.push(bytes);
333 }
334 }
335 }
336
337 let tmp_path = self.path.with_extension("tmp");
339 {
340 let mut tmp = File::create(&tmp_path).map_err(FileStorageError::Io)?;
341
342 let raw_tasks = self
343 .entries
344 .try_read()
345 .map_err(|_| FileStorageError::WouldBlockLock)?;
346 if let Some(headers) = self.adapter.header(&raw_tasks) {
347 tmp.write_all(&headers).map_err(FileStorageError::Io)?;
348 }
349 for (job_id, entry) in raw_tasks.iter().enumerate() {
350 let bytes = if let Some(b) = rewrites.remove(&job_id) {
351 b
352 } else {
353 let line = A::from_entry(entry).map_err(FileStorageError::AdapterError)?;
354 self.adapter
355 .serialize(&line)
356 .map_err(FileStorageError::AdapterError)?
357 };
358 tmp.write_all(&bytes).map_err(FileStorageError::Io)?;
359 }
360
361 tmp.flush().map_err(FileStorageError::Io)?;
362 }
363
364 std::fs::rename(&tmp_path, &self.path).map_err(FileStorageError::Io)?;
366
367 drop(guard);
371 drop(outer_guard);
372 let new_file = OpenOptions::new()
373 .read(true)
374 .write(true)
375 .open(&self.path)
376 .map_err(FileStorageError::Io)?;
377 self.lock = Arc::new(RwLock::new(FileLock::new(new_file)));
378 } else {
379 for change in self
381 .pending
382 .try_write()
383 .map_err(|_| FileStorageError::WouldBlockLock)?
384 .drain(..)
385 {
386 if let PendingChange::Append(bytes) = change {
387 guard.seek(SeekFrom::End(0)).map_err(FileStorageError::Io)?;
388 guard.write_all(&bytes).map_err(FileStorageError::Io)?;
389 }
390 }
391 guard.flush().map_err(FileStorageError::Io)?;
392 }
393
394 self.last_flush = std::time::Instant::now();
395 Ok(())
396 }
397
398 pub fn tick(&mut self) -> Result<(), FileStorageError<A>> {
400 if let SyncPolicy::Periodic(interval) = self.sync_policy.clone() {
401 if self.last_flush.elapsed() >= interval {
402 self.flush()?;
403 }
404 }
405 Ok(())
406 }
407
408 fn load_entries(adapter: &A, file: &File) -> Result<Vec<RawTask>, FileStorageError<A>> {
410 let mut reader = BufReader::new(file);
411 reader
412 .seek(SeekFrom::Start(0))
413 .map_err(FileStorageError::Io)?;
414
415 let mut entries = Vec::new();
416 let mut raw: Vec<u8> = Vec::new();
417
418 loop {
419 raw.clear();
420 let n = reader
421 .read_until(b'\n', &mut raw)
422 .map_err(FileStorageError::Io)?;
423 if n == 0 {
424 break;
425 }
426 let trimmed = raw.trim_ascii();
427 if trimmed.is_empty() {
428 continue;
429 }
430
431 if adapter.is_header(trimmed) {
432 continue;
433 }
434 let line = adapter
435 .deserialize(trimmed)
436 .map_err(FileStorageError::AdapterError)?;
437 entries.push(
438 adapter
439 .to_entry(line)
440 .map_err(FileStorageError::AdapterError)?,
441 );
442 }
443 Ok(entries)
444 }
445}
446
447impl<A: Adapter + Unpin, Args: Unpin> Stream for FileStorage<Args, A> {
448 type Item = Result<(usize, RawTask), FileStorageError<A>>;
449
450 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
451 let this = self.get_mut();
452 let raw_tasks = this
453 .entries
454 .try_read()
455 .map_err(|_| FileStorageError::WouldBlockLock)?;
456 if this.read_cursor < raw_tasks.len() {
457 let line_id = this.read_cursor;
458 let job = raw_tasks[line_id].clone();
459 this.read_cursor += 1;
460 Poll::Ready(Some(Ok((line_id, job))))
461 } else {
462 Poll::Pending
463 }
464 }
465}
466
467impl<Args, A> Backend for FileStorage<Args, A>
468where
469 Args: 'static + Send + Serialize + for<'de> Deserialize<'de> + Unpin,
470 A: Adapter + Unpin,
471 A::Error: std::error::Error + Send + Sync + 'static,
472{
473 type Task = Task<Value>;
474 type Error = FileStorageError<A>;
475
476 fn poll_ready(
477 &mut self,
478 cx: &mut Context<'_>,
479 _: &WorkerContext,
480 ) -> Poll<Result<(), Self::Error>> {
481 if self.lock.try_read().is_ok() {
482 Poll::Ready(Ok(()))
483 } else {
484 cx.waker().wake_by_ref();
485 Poll::Pending
486 }
487 }
488
489 fn poll_next(
490 &mut self,
491 cx: &mut Context<'_>,
492 _: &WorkerContext,
493 ) -> Poll<Option<Result<Self::Task, Self::Error>>> {
494 self.poll_next_unpin(cx).map_ok(|(line_id, mut job)| {
495 job.ctx.insert("line_id", line_id.to_string()).unwrap();
496 let task = TaskBuilder::new(job.args)
497 .with_metadata(job.ctx)
498 .task_id(job.task_id);
499 task.build()
500 })
501 }
502
503 fn poll_close(
504 &mut self,
505 _: &mut Context<'_>,
506 _: &WorkerContext,
507 ) -> Poll<Result<(), Self::Error>> {
508 self.flush()?;
509 Poll::Ready(Ok(()))
510 }
511}
512
513impl<Args, A> BackendConfig for FileStorage<Args, A>
514where
515 Args: 'static + Send + Serialize + for<'de> Deserialize<'de> + Unpin,
516 A: Adapter + Unpin + Clone,
517 A::Error: std::error::Error + Send + Sync + 'static,
518{
519 type Args = Args;
520 type Id = RandomId;
521 type Kind = Durable;
522 type Config = PathBuf;
523 type Layer = AcknowledgeLayer<Self>;
524 fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer {
525 AcknowledgeLayer::new(self.clone())
526 }
527
528 fn config(&self) -> &Self::Config {
529 &self.path
530 }
531}
532
533impl<Args, A> WireFormatBackend for FileStorage<Args, A>
534where
535 Args: 'static + Send + Serialize + for<'de> Deserialize<'de> + Unpin,
536 A: Adapter + Unpin,
537 A::Error: std::error::Error + Send + Sync + 'static,
538{
539 type Codec = JsonCodec<Value>;
540 type Compact = Value;
541 fn codec(&self) -> &Self::Codec {
542 &self.codec
543 }
544}
545
546#[derive(Debug, Clone, Default)]
548pub struct CsvAdapter {
549 header: Arc<Mutex<Option<Vec<String>>>>,
552}
553
554pub type CsvStorage<Args> = FileStorage<Args, CsvAdapter>;
556
557impl Adapter for CsvAdapter {
558 type Line = BTreeMap<String, String>;
559
560 type Error = io::Error;
561
562 fn serialize(&self, line: &Self::Line) -> Result<Vec<u8>, Self::Error> {
563 let mut out = Vec::new();
564
565 let mut header = self
566 .header
567 .lock()
568 .map_err(|_| io::Error::other("header mutex poisoned"))?;
569
570 if header.is_none() {
571 let cols: Vec<String> = line.keys().cloned().collect();
572 out.extend_from_slice(format!("{}\n", cols.join(",")).as_bytes());
573 *header = Some(cols);
574 }
575
576 let row = line
577 .values()
578 .map(|v| v.as_str())
579 .collect::<Vec<_>>()
580 .join(",");
581
582 out.extend_from_slice(format!("{row}\n").as_bytes());
583
584 Ok(out)
585 }
586
587 fn deserialize(&self, raw: &[u8]) -> Result<Self::Line, Self::Error> {
588 let row = std::str::from_utf8(raw)
589 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
590 .trim_end_matches('\n');
591
592 let header = self
593 .header
594 .lock()
595 .map_err(|_| io::Error::other("header mutex poisoned"))?;
596
597 let cols = header.as_ref().ok_or_else(|| {
598 io::Error::new(
599 io::ErrorKind::InvalidData,
600 "CsvAdapter: deserialize called before header was parsed",
601 )
602 })?;
603
604 let map = row
605 .split(',')
606 .zip(cols.iter())
607 .map(|(value, key)| (key.clone(), value.to_owned()))
608 .collect::<BTreeMap<_, _>>();
609
610 Ok(map)
611 }
612
613 fn to_entry(&self, line: Self::Line) -> Result<RawTask, Self::Error> {
614 let args = util::to_value(Some("args"), &line);
615
616 let mut result = None;
617
618 if line.contains_key("result") {
619 result = Some(util::to_value(Some("result"), &line));
620 }
621
622 let idempotency_key = line.get("idempotency_key").cloned();
623
624 let task_id = line
625 .get("task_id")
626 .and_then(|s| FromStr::from_str(s).ok())
627 .unwrap_or(TaskId::from_string(RandomId::default()));
628
629 let ctx = line
630 .iter()
631 .filter(|(k, _)| k.starts_with("ctx."))
632 .map(|(k, v)| (k.clone(), v.clone()))
633 .collect::<HashMap<String, String>>();
634
635 let ctx = MetadataStore::from_map(ctx);
636
637 Ok(RawTask {
638 task_id,
639 args,
640 ctx,
641 result,
642 idempotency_key,
643 })
644 }
645
646 fn from_entry(entry: &RawTask) -> Result<Self::Line, Self::Error> {
647 let mut line = BTreeMap::new();
648
649 line.insert("task_id".to_owned(), entry.task_id.to_string());
650
651 let args = util::from_value(Some("args"), &entry.args);
652
653 line.extend(args);
654
655 let result = entry
656 .result
657 .as_ref()
658 .map(|r| util::from_value(Some("result"), r))
659 .unwrap_or_else(|| util::from_value(Some("result"), &serde_json::Value::Null));
660
661 line.extend(result);
662
663 if let Some(idempotency_key) = &entry.idempotency_key {
664 line.insert("idempotency_key".to_owned(), idempotency_key.clone());
665 }
666
667 for (k, value) in entry.ctx.iter() {
668 line.insert(format!("ctx.{k}"), value.clone());
669 }
670
671 Ok(line)
672 }
673
674 fn is_header(&self, raw: &[u8]) -> bool {
675 let Ok(mut header) = self.header.lock() else {
676 return false;
677 };
678
679 if header.is_none() {
680 if let Ok(row) = std::str::from_utf8(raw) {
681 let cols: Vec<String> = row
682 .trim_end_matches('\n')
683 .split(',')
684 .map(|s| s.to_owned())
685 .collect();
686
687 *header = Some(cols);
688 }
689
690 return true; }
692
693 false
694 }
695
696 fn header(&self, entries: &Vec<RawTask>) -> Option<Vec<u8>> {
697 let first = entries.first()?;
698
699 let line = Self::from_entry(first).ok()?;
700
701 let cols: Vec<String> = line.keys().cloned().collect();
702 Some(format!("{}\n", cols.join(",")).as_bytes().to_vec())
703 }
704}
705
706impl<Args, Res, A> Acknowledge<Res> for FileStorage<Args, A>
707where
708 Args: Send + 'static + Debug,
709 Res: Serialize,
710 A: Adapter + Clone,
711{
712 type Error = FileStorageError<A>;
713
714 type Future = Ready<Result<(), Self::Error>>;
715
716 fn ack(&mut self, res: &Result<Res, BoxDynError>, ctx: &ExecutionContext) -> Self::Future {
717 let res = |this: &mut Self| {
718 let val = serde_json::to_value(res.as_ref().map_err(|e| e.to_string()))?;
719 let line_id = ctx
720 .metadata()
721 .get("line_id")
722 .unwrap()
723 .parse()
724 .map_err(|e: ParseIntError| FileStorageError::Parse(e.to_string()))?;
725 this.set_result(line_id, val)?;
726 Ok(())
727 };
728
729 ready(res(self))
730 }
731}
732
733impl<Args, A, O> WaitForCompletion<O> for FileStorage<Args, A>
734where
735 O: Send + DeserializeOwned + 'static + Unpin + Serialize,
736 A: Adapter + Unpin + Sync + Clone,
737 A::Error: std::error::Error + Send + Sync + 'static,
738 Args: Serialize + DeserializeOwned + 'static + Send + Sync + Unpin,
739{
740 type ResultStream = BoxStream<'static, Result<TaskResult<O>, FileStorageError<A>>>;
741 fn wait_for(&mut self, task_ids: impl IntoIterator<Item = TaskId>) -> Self::ResultStream {
742 use futures_util::StreamExt;
743 use std::{collections::HashSet, time::Duration};
744
745 let task_ids: HashSet<_> = task_ids.into_iter().collect();
746 struct PollState<Res, T, A: Adapter> {
747 vault: FileStorage<T, A>,
748 pending_tasks: HashSet<TaskId>,
749 poll_interval: Duration,
750 _phantom: std::marker::PhantomData<Res>,
751 }
752 let state = PollState {
753 vault: self.clone(),
754 pending_tasks: task_ids,
755 poll_interval: Duration::from_millis(100),
756 _phantom: std::marker::PhantomData,
757 };
758 futures_util::stream::unfold(state, |mut state: PollState<Value, Args, A>| {
759 async move {
760 if state.pending_tasks.is_empty() {
761 return None;
762 }
763
764 loop {
765 let vault = &state.vault;
767
768 let completed_task = {
769 let vault = vault.entries.try_read().ok()?;
770 vault.iter().find_map(|value| {
771 let attempt = value
772 .ctx
773 .get("attempt")
774 .map(FromStr::from_str)
775 .transpose()
776 .unwrap_or_default()
777 .unwrap_or_default();
778 let task_id = value.task_id.clone();
779 if state.pending_tasks.contains(&task_id) {
780 Some((task_id, value.result.clone()?, attempt))
781 } else {
782 None
783 }
784 })
785 };
786
787 if let Some((task_id, result, attempt)) = completed_task {
788 state.pending_tasks.remove(&task_id);
789 let result: Result<O, String> = serde_json::from_value(result).unwrap();
790 return Some((
791 Ok(TaskResult {
792 task_id,
793 status: Status::Done,
794 result,
795 attempt,
796 }),
797 state,
798 ));
799 }
800
801 apalis_core::timer::sleep(state.poll_interval).await;
803 }
804 }
805 })
806 .boxed()
807 }
808
809 async fn check_status(
810 &mut self,
811 task_ids: impl IntoIterator<Item = TaskId> + Send,
812 ) -> Result<Vec<TaskResult<O>>, Self::Error> {
813 use apalis_core::task::status::Status;
814 use std::collections::HashSet;
815 let task_ids: HashSet<_> = task_ids.into_iter().collect();
816 let mut results = Vec::new();
817 for task_id in task_ids {
818 if let Some(value) = self
819 .entries
820 .try_read()
821 .unwrap()
822 .iter()
823 .find(|s| s.task_id == task_id)
824 {
825 let attempt = value
826 .ctx
827 .get("attempt")
828 .map(FromStr::from_str)
829 .transpose()
830 .unwrap_or_default()
831 .unwrap_or_default();
832 if value.result.is_none() {
833 results.push(TaskResult {
834 task_id: task_id.clone(),
835 status: Status::Pending,
836 result: Err("Task not completed yet".to_owned()),
837 attempt,
838 });
839 continue;
840 }
841 let result = match serde_json::from_value::<Result<O, String>>(
842 value.result.clone().unwrap(),
843 ) {
844 Ok(result) => TaskResult {
845 task_id: task_id.clone(),
846 status: Status::Done,
847 result,
848 attempt,
849 },
850 Err(e) => TaskResult {
851 task_id: task_id.clone(),
852 status: Status::Failed,
853 result: Err(format!("Deserialization error: {e}")),
854 attempt,
855 },
856 };
857 results.push(result);
858 }
859 }
860 Ok(results)
861 }
862}
863
864impl<Args: Send + 'static, A> TryNewBackend for FileStorage<Args, A>
865where
866 A: Adapter + Default + Unpin + Sync + Clone,
867 A::Error: std::error::Error + Send + Sync + 'static,
868 Args: Serialize + DeserializeOwned + 'static + Send + Sync + Unpin,
869{
870 type Backend = Self;
871 fn try_new(config: Self::Config) -> Result<Self::Backend, Self::Error> {
872 Self::new(config)
873 }
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879 use std::time::Duration;
880
881 use apalis_core::{
882 backend::TaskSink,
883 error::BoxDynError,
884 worker::{
885 builder::WorkerBuilder, context::WorkerContext, ext::event_listener::EventListenerExt,
886 },
887 };
888
889 const ITEMS: u32 = 100;
890
891 #[tokio::test]
892 async fn json_worker() {
893 let mut json_store = JsonStorage::new_temp().unwrap();
894 for i in 0..ITEMS {
895 json_store.push(i).await.unwrap();
896 }
897
898 async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> {
899 tokio::time::sleep(Duration::from_secs(1)).await;
900 if task == ITEMS - 1 {
901 worker.stop().unwrap();
902 return Err("Worker stopped!")?;
903 }
904 Ok(())
905 }
906
907 let worker = WorkerBuilder::new("rango-tango-json")
908 .backend(json_store)
909 .on_event(|worker, ev| {
910 println!("On Event = {ev:?} from = {}", worker.name());
911 })
912 .build(task);
913 worker.run().await.unwrap();
914 }
915
916 #[tokio::test]
917 async fn csv_worker() {
918 #[derive(Debug, Serialize, Deserialize, Default)]
919 struct Email {
920 to: String,
921 subject: String,
922 index: u32,
923 }
924
925 let mut csv_store = CsvStorage::new_temp().unwrap();
926 for i in 0..ITEMS {
927 csv_store
928 .push(Email {
929 subject: "Test".to_string(),
930 to: "test".to_string(),
931 index: i,
932 })
933 .await
934 .unwrap();
935 }
936
937 async fn task(task: Email, worker: WorkerContext) -> Result<(), BoxDynError> {
938 tokio::time::sleep(Duration::from_secs(1)).await;
939 if task.index == ITEMS - 1 {
940 worker.stop().unwrap();
941 return Err("Worker stopped!")?;
942 }
943 Ok(())
944 }
945
946 let worker = WorkerBuilder::new("rango-tango-csv")
947 .backend(csv_store)
948 .on_event(|worker, ev| {
949 println!("On Event = {ev:?} from = {}", worker.name());
950 })
951 .build(task);
952 worker.run().await.unwrap();
953 }
954}