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 installThis 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 = 100Generating Jobs
Basic job
rails generate rust_on_background:job cleanupJob with typed arguments
rails generate rust_on_background:job send_email user_id:integer template:string urgent:booleanGenerates:
#[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:OrderThe 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:stopWriting 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
- Create
app/jobs/rust/config.production.tomlwith production database/Redis URLs -
rake rust_on_background:rebuild(stops workers, builds, starts workers) rake rust_on_background:scheduler:start
Workers run as background processes with PID files in tmp/pids/.
License
MIT License. See LICENSE for details.