apalis

repository·main·Indexed 23 days ago

https://github.com/apalis-dev/apalis

A simple, extensible, and multithreaded background task and message processing library for Rust (v1.0.0-rc.9). It features a pluggable backend system supporting SQLite, PostgreSQL, MySQL, Redis, and in-memory storage. The framework provides robust task management with retries, rate limits, and graceful shutdowns, integrating with the tower ecosystem for middleware. It also includes apalis-workflow for creating sequential and Directed Acyclic Graph (DAG) workflows.

Tokens
31.8K
Snippets
59
Records
171
Agent score
77%

What's inside apalis

  1. Overview of apalis-codec

    main

    apalis-codec is a utility crate designed for apalis backends. It provides the serialization and deserialization logic required to encode and decode job payloads. This ensures that task data can be reliably stored in backends and transmitted during job execution.

    Key capabilities include:

    • Support for multiple formats (e.g., JSON, MessagePack).
    • Type-safe serialization/deserialization.
    • Robust error handling for codec operations.
    • Integration with apalis backends via the Backend trait.
  2. Choose a SQL backend for apalis

    main

    The apalis-sql crate provides shared utilities, but you should use the specific crate corresponding to your database backend. The available stable backends are:

    • SQLite: Use apalis-sqlite
    • PostgreSQL: Use apalis-postgres
    • MySQL: Use apalis-mysql

    Note that apalis-surreal (SurrealDB) and Diesel compatibility features are currently under development.

  3. Use file-based backends for task persistence

    main

    The apalis-file-storage crate provides a file-based backend for persisting tasks and results. It is suitable for testing or ephemeral use cases where a full database is not required.

    Key Capabilities:

    • Sink support: Push new tasks into the storage.
    • Codec Support: Uses JSON for serializing task arguments.
    • Workflow Support: Compatible with apalis-workflow.
    • Ack Support: Supports acknowledging task completion.
    • WaitForCompletion: Allows waiting for tasks to finish without blocking execution.
  4. Implement Graceful Shutdown

    main

    The graceful shutdown system ensures workers stop safely without losing tasks.

    Key Mechanisms:

    • Task Tracking: Workers track active tasks to ensure they finish before exiting.
    • Monitor Coordination: A shared Shutdown token allows the Monitor to signal all workers to stop simultaneously.
    • Shutdown Timeout: You can prevent the shutdown process from hanging indefinitely by using .with_terminator() on the Monitor to set a time limit.
  5. Implement or use Backends

    main

    The Backend trait is the core abstraction for task sources. It defines how tasks are polled, streamed, and persisted.

    Inbuilt Implementations:

    • MemoryStorage: In-memory storage using channels.
    • Pipe: A backend for stream-to-backend pipelines.
    • CustomBackend: Allows composing custom functions for task management.

    Associated Types in the Backend trait:

    • Stream: The task stream type for polling.
    • Layer: The middleware layer stack.
    • Codec: The serialization format for task data.
    • Beat: Heartbeat stream for liveness checks.
    • Id: Type for unique task identifiers.
    • Conn: Context associated with tasks.
    • Error: Error type for backend operations.
  6. How apalis-core works: Core Concepts

    main

    apalis-core is a task processing framework built around four primary abstractions:

    • Tasks: Type-safe data structures containing task arguments and processing metadata.
    • Backends: Pluggable implementations for task storage and streaming (e.g., in-memory, SQL, Redis).
    • Workers: The runtime engines responsible for polling, executing, and managing the lifecycle of tasks.
    • Monitor: A coordination layer used to manage multiple workers, handle events, and facilitate graceful shutdowns.

    The framework integrates with the tower ecosystem, allowing you to use middleware for features like rate limiting, timeouts, and observability.

  7. Create a Sequential Workflow

    main

    A sequential workflow is a chain of steps executed one after another. You can build it using the Workflow::new constructor and composing steps with methods like .and_then() (to transform/process results) and .filter_map() (to conditionally continue the workflow).

    Workflows are executed by a Worker built via WorkerBuilder, which requires a backend (such as JsonStorage) to ensure durability and resumability.

    use apalis::prelude::*;
    use apalis_workflow::*;
    use apalis_file_storage::JsonStorage;
    
    #[tokio::main]
    async fn main() {
       let workflow = Workflow::new("odd-numbers-workflow")
           .and_then(|a: usize| async move { Ok::<_, BoxDynError>((0..a).collect::<Vec<_>>()) })
           .filter_map(|x| async move { if x % 2 != 0 { Some(x) } else { None } })
           .and_then(|a: Vec<usize>| async move {
               println!("Sum: {}", a.iter().sum::<usize>());
               Ok::<_, BoxDynError>(())
            });
    
       let mut in_memory = JsonStorage::new_temp().unwrap();
    
       in_memory.push_start(10).await.unwrap();
    
       let worker = WorkerBuilder::new("rango-tango")
           .backend(in_memory)
           .on_event(|ctx, ev| {
               println!("On Event = {:?}", ev);
           })
           .build(workflow);
       worker.run().await.unwrap();
    }
  8. Create a Directed Acyclic Graph (DAG) Workflow

    main

    For complex workflows where steps have dependencies, use DagFlow.

    1. Initialize a new flow with DagFlow::new("name").
    2. Define nodes using .node(function).
    3. Establish dependencies using .depends_on((&node_a, &node_b, ...)).
    4. Crucial: Call .validate()? to ensure the graph structure is valid before running.
    5. The collector node (the final step) can receive a tuple of results from its dependencies. The types and order of the tuple must match the dependencies provided in depends_on.

    To stop a workflow manually from within a node, you can use WorkerContext::stop().

    use apalis::prelude::*;
    use apalis_file_storage::JsonStorage;
    use apalis_workflow::{DagFlow, WorkflowSink};
    use serde_json::Value;
    
    async fn get_name(user_id: u32) -> Result<String, BoxDynError> {
        Ok(user_id.to_string())
    }
    
    async fn get_age(user_id: u32) -> Result<usize, BoxDynError> {
        Ok(user_id as usize + 20)
    }
    
    async fn get_address(user_id: u32) -> Result<usize, BoxDynError> {
        Ok(user_id as usize + 100)
    }
    
    async fn collector(
        (name, age, address): (String, usize, usize),
        wrk: WorkerContext,
    ) -> Result<usize, BoxDynError> {
        let result = name.parse::<usize>()? + age + address;
        wrk.stop().unwrap();
        Ok(result)
    }
    
    #[tokio::main]
    async fn main() -> Result<(), BoxDynError> {
        let mut backend = JsonStorage::new_temp().unwrap();
    
        backend
            .push_start(vec![42, 43, 44])
            .await
            .unwrap();
    
        let dag_flow = DagFlow::new("user-etl-workflow");
        let get_name = dag_flow.node(get_name);
        let get_age = dag_flow.node(get_age);
        let get_address = dag_flow.node(get_address);
        dag_flow
            .node(collector)
            .depends_on((&get_name, &get_age, &get_address)); // Order and types matters here
    
        dag_flow.validate()?; // Ensure DAG is valid
    
        info!("Executing workflow:\n{}", dag_flow); // Print the DAG structure in dot format
    
        WorkerBuilder::new("tasty-banana")
            .backend(backend)
            .enable_tracing()
            .on_event(|_c, e| info!("{e}"))
            .build(dag_flow)
            .run()
            .await?;
        Ok(())
    }