RustOnBackground

Run background jobs in Rust. Run a few commands and you're all set up - just scaffold and write Rust. Jump to quick start

# Rails: enqueue a job
RustOnBackground.perform("send_email", user_id: 123, template: "welcome")

# Schedule for later
RustOnBackground.perform_at(1.hour.from_now, "cleanup", batch_size: 100)

Features

  • Rails generators for instant setup, very easy
  • Auto-detects database adapter (MySQL, PostgreSQL, SQLite) from your database.yml config
  • Scaffolding: Auto-generates Rust structs from ActiveRecord models (You can do just partial scaffold only to create the structs from schema)
  • Multiple queues with configurable worker counts
  • Scheduled jobs with perform_at
  • Environment-specific configs (config.development.toml, config.production.toml)
  • Automatic retries with configurable retry count
  • Rake tasks for everything: build, start, stop, restart, status, rebuild

Requirements

  • Ruby 2.5+
  • Rails 5.0+
  • Redis 4.0+
  • Rust 1.70+ (install)

Quick Start

Add to your Gemfile:

gem "rust_on_background"

Run bundle install:

bundle install

This creates the Rust project structure at app/jobs/rust/ and detects your database configuration automatically:

rails generate rust_on_background:install
# Create a job (generates app/jobs/rust/src/jobs/send_email.rs)
rails generate rust_on_background:job send_email user_id:integer message:string

# Build
rake rust_on_background:build

# Start workers
rake rust_on_background:start

# Enqueue from Rails
RustOnBackground.perform("send_email", user_id: 1, message: "welcome")

Project Structure

After installation, your Rails app will have:

app/jobs/rust/
├── Cargo.toml                    # Rust dependencies (add crates here)
├── config.development.toml       # Dev config (auto-generated)
├── config.production.toml        # Create this for production
└── src/
    ├── main.rs                   # Don't need to touch this
    ├── worker.rs                 # Don't need to touch this
    ├── scheduler.rs              # Don't need to touch this
    └── jobs/                     # YOUR CODE GOES HERE
        ├── mod.rs                # Auto-updated by generator, this is basically a controller for the jobs(scaffolding creates the action for you in here)
        └── send_email.rs         # Example job file

Configuration

Environment-specific config files in app/jobs/rust/:

# config.development.toml

[database]
url = "mysql://root@localhost/myapp_development"

[redis]
url = "redis://127.0.0.1:6379"

[logging]
path = "log/development.log" # can leave empty for no logs

# Define your queues
[[queues]]
name = "default"
workers = 4

[[queues]]
name = "emails"
workers = 2

# Scheduler settings (optional)
# [scheduler]
# poll_interval_ms = 1000
# batch_size = 100

Generating Jobs

Basic job

rails generate rust_on_background:job cleanup

Job with typed arguments

rails generate rust_on_background:job send_email user_id:integer template:string urgent:boolean

Generates:

#[derive(Debug, Deserialize)]
struct Args {
    user_id: i64,
    template: String,
    urgent: bool,
}

pub async fn run(args: &serde_json::Value, _database_url: &str) -> JobResult {
    let args: Args = serde_json::from_value(args.clone())?;
    // Your logic here
    JobResult::Success
}

Job with ActiveRecord model

rails generate rust_on_background:job process_order order:Order

The generator reads your model's columns from the database and creates a matching Rust struct:

#[derive(Debug, Deserialize)]
struct Order {
    id: i64,
    user_id: i64,
    total: f64,
    status: String,
    created_at: String,
    updated_at: String,
}

#[derive(Debug, Deserialize)]
struct Args {
    order: Order,
}

Enqueuing Jobs

Immediate execution

RustOnBackground.perform("job_name", arg1: "value", arg2: 123) # (default queue if not specified)

With specific queue

RustOnBackground.perform("send_email", queue: "emails", user_id: 1)

With retry count

RustOnBackground.perform("flaky_job", retry_count: 5, data: "...")

Scheduled execution

# Run in 1 hour
RustOnBackground.perform_at(1.hour.from_now, "cleanup")

# Run at specific time
RustOnBackground.perform_at(Date.tomorrow.noon, "daily_report")

# With all options
RustOnBackground.perform_at(
  30.minutes.from_now,
  "send_reminder",
  queue: "emails",
  retry_count: 3,
  user_id: 123
)

Supported Types

Ruby/Rails Type Rust Type
string, text String
integer, bigint i64
float, decimal f64
boolean bool
array, set Vec<serde_json::Value>
hash, json, jsonb serde_json::Value
date, datetime, time String
uuid String
binary, blob Vec<u8>
ActiveRecord model Auto-generated struct

Rake Tasks

# Build the Rust binary
rake rust_on_background:build

# Start all workers
rake rust_on_background:start

# Start specific queue
rake rust_on_background:start[emails]

# Stop all workers
rake rust_on_background:stop

# Stop specific queue
rake rust_on_background:stop[emails]

# Restart workers
rake rust_on_background:restart

# Stop, rebuild, and start - for example when doing a deploy
rake rust_on_background:rebuild

# Check status
rake rust_on_background:status

# Start scheduler (run ONE instance per Redis db)
rake rust_on_background:scheduler:start

# Stop scheduler
rake rust_on_background:scheduler:stop

Writing Job Logic

Jobs are in app/jobs/rust/src/jobs/. Each job is a Rust module with a run function:

use serde::Deserialize;
use crate::worker::JobResult;

#[derive(Debug, Deserialize)]
struct Args {
    user_id: i64,
    message: String,
}

pub async fn run(args: &serde_json::Value, database_url: &str) -> JobResult {
    // Parse args
    let args: Args = match serde_json::from_value(args.clone()) {
        Ok(a) => a,
        Err(e) => return JobResult::Failed(format!("Invalid args: {}", e)),
    };

    // Your logic here
    tracing::info!("Processing user {}", args.user_id);

    // Return result
    JobResult::Success
    // or: JobResult::Retry("temporary failure".to_string())
    // or: JobResult::Failed("permanent failure".to_string())
}

Database access

The database_url parameter gives you direct access to your Rails database:

use sqlx::mysql::MySqlPool;

pub async fn run(args: &serde_json::Value, database_url: &str) -> JobResult {
    let pool = MySqlPool::connect(database_url).await?;

    let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE active = ?")
        .bind(true)
        .fetch_all(&pool)
        .await?;

    JobResult::Success
}

HTTP callbacks to Rails (example)

If you need to notify Rails when a job completes, you can use any HTTP client. Example with reqwest:

pub async fn run(args: &serde_json::Value, _database_url: &str) -> JobResult {
    // Do work...

    // Notify Rails
    reqwest::Client::new()
        .post("http://localhost:3000/webhooks/job_complete")
        .json(&serde_json::json!({ "job": "my_job", "status": "done" }))
        .send()
        .await?;

    JobResult::Success
}

Production Deployment

  1. Create app/jobs/rust/config.production.toml with production database/Redis URLs
  2. rake rust_on_background:rebuild (stops workers, builds, starts workers)
  3. rake rust_on_background:scheduler:start

Workers run as background processes with PID files in tmp/pids/.

License

MIT License. See LICENSE for details.