RunAt
A distributed job scheduler for Rust with PostgreSQL backend support.
Features
- Distributed Job Scheduling: Run jobs across multiple workers with PostgreSQL-backed coordination
- Cron Support: Schedule recurring jobs using cron expressions
- Retry Mechanisms: Built-in exponential backoff and custom retry strategies
- Type-Safe Jobs: Leverage Rust's type system for job definitions
- Async/Await: Built on Tokio for efficient async job execution
- Macro Support: Convenient procedural macros for defining jobs
Installation
Add this to your Cargo.toml:
[dependencies]
runat = "0.1.0"
For PostgreSQL support (enabled by default):
[dependencies]
runat = { version = "0.1.0", features = ["postgres"] }
Optional features:
postgres - PostgreSQL backend (enabled by default)
tracing - Tracing support for observability
Quick Start
Basic Job
use runat::{BackgroundJob, JobQueue, PostgresDatastore, Result, job};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
#[job]
async fn send_email(to: String, subject: String, body: String) -> Result<()> {
println!("Sending email to {}: {}", to, subject);
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://user:pass@localhost/db".to_string());
let pool = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await?;
let datastore = PostgresDatastore::new(pool).await?;
datastore.migrate().await?;
let queue = Arc::new(JobQueue::with_datastore(Arc::new(datastore)));
queue.enqueue(
JobSendEmail {
to: "user@example.com".to_string(),
subject: "Welcome!".to_string(),
body: "Thanks for signing up!".to_string(),
}.job()?
).await?;
Ok(())
}
Scheduled Jobs with Cron
queue.enqueue(
JobSendEmail {
to: "admin@example.com".to_string(),
subject: "Daily Report".to_string(),
body: "Here's your daily report".to_string(),
}
.job()?
.cron("*/10 * * * * * *")?
).await?;
Custom Job Implementation
use runat::{Job, Executable, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProcessPayment {
user_id: String,
amount: f64,
}
#[async_trait::async_trait]
impl Executable for ProcessPayment {
type Output = Result<String>;
async fn execute(&mut self) -> Self::Output {
println!("Processing ${} payment for user {}", self.amount, self.user_id);
Ok(format!("Payment processed for {}", self.user_id))
}
}
Running Workers
use runat::Worker;
let worker = Worker::new(queue.clone());
tokio::spawn(async move {
worker.run().await
});
Retry Strategies
use runat::Retry;
use chrono::Duration;
let mut job = Job::new(email_job)?;
job.max_attempts = 5;
job = job.retry(Retry::exponential(Duration::seconds(5)));
queue.enqueue(job).await?;
Running Tests
cargo test