sqlxmq

repository·master·Indexed 41 days ago

https://github.com/diggsey/sqlxmq

A reliable job queue for Rust built on top of sqlx and PostgreSQL. It provides transactional integrity for background jobs, featuring a procedural macro for job definition, a JobRegistry for management, and a JobRunner for execution. Version 0.6.0 supports JSON payloads, configurable retries with backoff, strict ordering, and checkpointing for long-running tasks.

Tokens
6.5K
Snippets
23
Records
27
Agent score
51%

What's inside sqlxmq

  1. Configure the database schema

    master

    sqlxmq requires specific database tables and stored procedures to exist in your PostgreSQL database. All items created by the library are prefixed with mq to avoid namespace collisions.

    To set up your database, copy the migration files provided in the sqlxmq crate into your project's migration folder.

  2. Set up a job runner with JobRegistry

    master

    A job runner is responsible for listening for new jobs and executing them. You set this up using a JobRegistry.

    Workflow:

    1. Initialize Registry: Create a JobRegistry by passing a slice of job functions: JobRegistry::new(&[job_function]).
    2. Configure Registry: Use registry.set_context(value) to provide context that will be passed to job arguments.
    3. Create Runner: Call .runner(&pool) on the registry, providing a sqlx PostgreSQL connection pool.
    4. Configure Runner: Use .set_concurrency(min, max) to specify the minimum and maximum number of concurrent jobs.
    5. Start Runner: Call .run().await? to start the runner. The runner will continue to listen and execute jobs until the runner instance is dropped.
    use std::error::Error;
    
    use sqlxmq::JobRegistry;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        // You'll need to provide a Postgres connection pool.
        let pool = connect_to_db().await?;
    
        // Construct a job registry from our single job.
        let mut registry = JobRegistry::new(&[example_job]);
        // Here is where you can configure the registry
        // registry.set_error_handler(...)
    
        // And add context
        registry.set_context("Hello");
    
        let runner = registry
            // Create a job runner using the connection pool.
            .runner(&pool)
            // Here is where you can configure the job runner
            // Aim to keep 10-20 jobs running at a time.
            .set_concurrency(10, 20)
            // Start the job runner in the background.
            .run()
            .await?;
    
        // The job runner will continue listening and running
        // jobs until `runner` is dropped.
        Ok(())
    }
  3. Spawn a job using `JobBuilder`

    master

    To trigger a job, use the .builder() method provided by the job function (generated by the #[job] macro). This allows you to override default configurations for a specific instance of the job.

    Available methods on the builder:

    • .set_channel_name(name): Override the default channel.
    • .set_json(payload): Set a JSON payload (must be serializable).
    • .set_retries(count): Override the retry count.
    • .set_retry_backoff(duration): Override the backoff duration.
    • .set_ordered(bool): Enable strict ordering for this job.
    • .spawn(&pool): Execute the command to insert the job into the database.
    example_job.builder()
        .set_channel_name("bar")
        .set_json("John")?
        .spawn(&pool)
        .await?;
  4. Set up a JobRegistry and JobRunner

    master

    A JobRegistry manages the collection of available jobs and any shared context. A JobRunner is the component that actually listens for and executes those jobs.

    Workflow:

    1. Create a JobRegistry by passing a slice of your job functions.
    2. (Optional) Use registry.set_context(value) to provide data accessible to all jobs.
    3. Create a runner using .runner(&pool) where pool is a sqlx::PgPool.
    4. Configure the runner (e.g., .set_concurrency(min, max)).
    5. Call .run().await to start the runner. The runner will continue running until the returned handle is dropped.
    use std::error::Error;
    use sqlxmq::JobRegistry;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        let pool = connect_to_db().await?;
    
        let mut registry = JobRegistry::new(&[example_job]);
        registry.set_context("Hello");
    
        let runner = registry
            .runner(&pool)
            .set_concurrency(10, 20)
            .run()
            .await?;
    
        Ok(())
    }
  5. Configure and run a JobRunner

    master

    To execute jobs, use JobRunnerOptions to configure a runner and then call .run(). You must provide a sqlx::Pool<Postgres> and a dispatch function that accepts a CurrentJob.

    Key configuration options:

    • set_concurrency(min, max): Sets the concurrency limits. The runner polls for more jobs when active jobs fall below min.
    • set_channel_names(&[&str]): Subscribes the runner to specific channels. If omitted, it subscribes to all channels.
    • set_keep_alive(bool): Enables/disables automatic background keep-alive signals for running jobs (defaults to true).

    Calling .run() returns a JobRunnerHandle, which controls the lifecycle of the background runner task.

    let mut options = JobRunnerOptions::new(&pool, |job| {
        // Your job logic here
    });
    
    options
        .set_concurrency(8, 16)
        .set_channel_names(&["email_notifications", "image_processing"]);
    
    let mut handle = options.run().await?;
    
    // The runner runs in the background until `handle` is dropped or `.stop()` is called.
  6. Define a job using the #[job] attribute

    master

    To define a job, create an asynchronous function and decorate it with the #[job] attribute.

    Function Signature Requirements:

    • The first argument must always be a CurrentJob instance.
    • Subsequent arguments are optional and can be used to access context provided via JobRegistry::set_context.
    • The function must return a Result<(), Box<dyn Error + Send + Sync + 'static>>.

    Key Operations within a Job:

    • Payload Decoding: Use current_job.json()? to decode a JSON payload into a type that implements Deserialize.
    • Completion: Call current_job.complete().await? to mark the job as successfully finished.

    Configuration: Use the #[job] attribute to set default options, such as channel_name.

    use std::error::Error;
    
    use sqlxmq::{job, CurrentJob};
    
    // Arguments to the `#[job]` attribute allow setting default job options.
    #[job(channel_name = "foo")]
    async fn example_job(
        // The first argument should always be the current job.
        mut current_job: CurrentJob,
        // Additional arguments are optional, but can be used to access context
        // provided via [`JobRegistry::set_context`].
        message: &'static str,
    ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
        // Decode a JSON payload
        let who: Option<String> = current_job.json()?;
    
        // Do some work
        println!("{}, {}!", message, who.as_deref().unwrap_or("world"));
    
        // Mark the job as complete
        current_job.complete().await?;
    
        Ok(())
    }
  7. Spawn a job using the job builder

    master

    To execute a job, use the .builder() method on the job function. This allows you to override default configurations before sending the job to the queue.

    Available Builder Methods:

    • .set_channel_name(name): Overrides the channel name.
    • .set_json(payload)?: Sets a JSON payload for the job.
    • .spawn(&pool).await?: Sends the job to the database using the provided connection pool.
    example_job.builder()
        // This is where we can override job configuration
        .set_channel_name("bar")
        .set_json("John")?
        .spawn(&pool)
        .await?;
  8. Commit two-phase commit jobs

    master

    If you have spawned jobs with a commit_interval set (enabling two-phase commit), you must explicitly call commit to finalize them. This function takes a slice of Uuids representing the jobs to be committed.

    use sqlxmq::commit;
    
    // job_ids contains the Uuids returned by JobBuilder::spawn
    commit(executor, &job_ids).await?;
  9. Configure JobBuilder options

    master

    The JobBuilder provides several methods to customize job behavior. Note that most methods return &mut Self to allow chaining.

    MethodDescription
    new(name: &str)Initializes a new builder with a random UUID and the given name.
    new_with_id(id: Uuid, name: &str)Initializes a builder with a specific UUID and name.
    set_channel_name(name: &str)Sets the channel name (default: "").
    set_channel_args(args: &str)Sets the channel arguments (default: "").
    set_retries(retries: u32)Sets the number of retries after the initial attempt (default: 4).
    set_retry_backoff(backoff: Duration)Sets the initial backoff duration for retries (default: 1s).
    set_delay(delay: Duration)Sets a delay before the job is executed (default: none).
    set_ordered(ordered: bool)Sets whether the job is strictly ordered within its channel (default: false).
    set_commit_interval(interval: Option<Duration>)Enables two-phase commit by setting a commit interval.
    set_raw_json(raw_json: &str)Sets a raw JSON string as the payload.
    set_raw_bytes(raw_bytes: &[u8])Sets raw binary data as the payload.
    set_json<T: Serialize>(value: &T)Serializes a type to JSON and sets it as the payload. Returns Result<&mut Self, serde_json::Error>.
    set_proto<F>(proto: F)A helper to apply multiple configuration changes at once using a closure.
  10. Initialize a JobBuilder from a NamedJob

    master

    A NamedJob (the type produced by the #[job] macro) provides methods to create a JobBuilder. This builder is used to construct specific job instances with the correct parameters and defaults defined for that job type.

    You can use either .builder() for a new job or .builder_with_id(id) if you are reconstructing a job from a specific UUID.

    // Using the static NamedJob generated by the macro
    let builder = MY_JOB.builder();
    let job = builder
        .param("some_param", "value")
        .build();
  11. Create a JobRunner from a JobRegistry

    master

    To start processing jobs from your database, convert your JobRegistry into a JobRunnerOptions object using the .runner() method. This method requires a sqlx::Pool<Postgres> connection pool.

    The resulting runner will automatically resolve job names against the registry and execute the corresponding job logic.

    // pool is a sqlx::Pool<Postgres>
    let runner_options = registry.runner(&pool);
    // Use runner_options to start your worker loop
  12. Manage job state with `CurrentJob`

    master

    The CurrentJob object is passed as the first argument to every job. It provides methods to interact with the job's lifecycle and data.

    • current_job.json::<T>(): Decodes the job's payload into type T (requires serde::Deserialize).
    • current_job.complete().await?: Marks the job as successfully completed.
    • current_job.checkpoint(checkpoint).await?: Saves a checkpoint. This is useful for long-running jobs so they can resume from this state if they fail and are retried.

    Checkpointing Example:

    let checkpoint = Checkpoint::new().set_json(&some_state)?;
    current_job.checkpoint(checkpoint).await?;