Cron Job Scheduling Guide

Armature provides a robust cron job scheduler for running periodic tasks in your application.

Features

  • โœ… Standard cron expression syntax
  • โœ… Named jobs with metadata
  • โœ… Async job execution
  • โœ… Job lifecycle management
  • โœ… Error handling and retry logic
  • โœ… Job overlap prevention
  • โœ… Job enable/disable at runtime
  • โœ… Job statistics and monitoring

Table of Contents

Basic Usage

Creating a Scheduler

use armature_cron::*;

#[tokio::main]
async fn main() -> Result<(), CronError> {
    let mut scheduler = CronScheduler::new();

    // Add jobs
    scheduler.add_job(
        "my_job",
        "0 * * * * *", // Every minute
        |ctx| Box::pin(async move {
            println!("Job executed!");
            Ok(())
        })
    )?;

    // Start the scheduler
    scheduler.start().await?;

    // Keep running
    tokio::signal::ctrl_c().await?;

    // Stop the scheduler
    scheduler.stop().await?;

    Ok(())
}

Adding Jobs

// Simple job
scheduler.add_job(
    "heartbeat",
    "*/5 * * * * *", // Every 5 seconds
    |ctx| Box::pin(async move {
        println!("Heartbeat");
        Ok(())
    })
)?;

// Job with shared state
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();

scheduler.add_job(
    "counter",
    "0 * * * * *", // Every minute
    move |ctx| {
        let counter = counter_clone.clone();
        Box::pin(async move {
            let count = counter.fetch_add(1, Ordering::SeqCst);
            println!("Count: {}", count + 1);
            Ok(())
        })
    }
)?;

Cron Expressions

Format

Cron expressions consist of 6 fields:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ second (0-59)
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ minute (0-59)
โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ hour (0-23)
โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ day of month (1-31)
โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ month (1-12 or JAN-DEC)
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ day of week (0-6 or SUN-SAT, 0=Sunday)
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚
* * * * * *

Special Characters

  • * - Any value
  • , - Value list separator (e.g., 1,3,5)
  • - - Range (e.g., 1-5)
  • / - Step values (e.g., */5 = every 5)

Common Expressions

use armature_cron::CronPresets;

// Every second
"* * * * * *"

// Every minute
"0 * * * * *"
// or
CronPresets::EVERY_MINUTE

// Every 5 minutes
"0 */5 * * * *"
CronPresets::EVERY_5_MINUTES

// Every hour
"0 0 * * * *"
CronPresets::EVERY_HOUR

// Every day at midnight
"0 0 0 * * *"
CronPresets::DAILY

// Every Monday at 9 AM
"0 0 9 * * MON"

// Weekdays at 9 AM
"0 0 9 * * MON-FRI"
CronPresets::WEEKDAYS_9AM

// First day of every month
"0 0 0 1 * *"
CronPresets::MONTHLY

// Every 15 minutes during business hours (9 AM - 5 PM)
"0 */15 9-17 * * *"

// Every weekend at 10 AM
"0 0 10 * * SAT,SUN"
CronPresets::WEEKENDS_10AM

Job Context

Every job receives a JobContext with information about the execution:

scheduler.add_job(
    "report",
    "0 * * * * *",
    |ctx| Box::pin(async move {
        println!("Job name: {}", ctx.name);
        println!("Scheduled time: {}", ctx.scheduled_time);
        println!("Actual time: {}", ctx.execution_time);
        println!("Execution count: {}", ctx.execution_count);
        println!("Delay: {:?}", ctx.delay());
        Ok(())
    })
)?;

JobContext Fields

pub struct JobContext {
    /// Job name
    pub name: String,

    /// When the job was scheduled to run
    pub scheduled_time: DateTime<Utc>,

    /// When the job actually started running
    pub execution_time: DateTime<Utc>,

    /// Number of times this job has executed (0-based)
    pub execution_count: u64,
}

Scheduler Configuration

Custom Configuration

use std::time::Duration;

let config = SchedulerConfig {
    // How often to check for jobs to run
    tick_interval: Duration::from_secs(1),

    // Whether to run jobs that were missed during downtime
    run_missed_jobs: false,

    // Maximum number of jobs to run concurrently
    max_concurrent_jobs: 10,

    // Whether to log job execution
    log_execution: true,
};

let mut scheduler = CronScheduler::with_config(config);

Default Configuration

SchedulerConfig {
    tick_interval: Duration::from_secs(1),
    run_missed_jobs: false,
    max_concurrent_jobs: 10,
    log_execution: true,
}

Job Management

Listing Jobs

let jobs = scheduler.list_jobs().await;
for job_name in jobs {
    println!("Job: {}", job_name);
}

Enabling/Disabling Jobs

// Disable a job (it won't run but stays in the scheduler)
scheduler.disable_job("my_job").await?;

// Re-enable a job
scheduler.enable_job("my_job").await?;

Removing Jobs

// Completely remove a job from the scheduler
scheduler.remove_job("my_job").await?;

Checking Status

// Check if scheduler is running
if scheduler.is_running().await {
    println!("Scheduler is active");
}

Error Handling

Handling Job Errors

scheduler.add_job(
    "api_sync",
    "0 */5 * * * *",
    |ctx| Box::pin(async move {
        match fetch_api_data().await {
            Ok(data) => {
                process_data(data).await?;
                Ok(())
            }
            Err(e) => {
                eprintln!("Failed to fetch API data: {}", e);
                Err(CronError::ExecutionFailed(e.to_string()))
            }
        }
    })
)?;

Error Types

pub enum CronError {
    /// Invalid cron expression
    InvalidExpression(String),

    /// Job not found
    JobNotFound(String),

    /// Job already exists
    JobAlreadyExists(String),

    /// Job execution failed
    ExecutionFailed(String),

    /// Scheduler not running
    SchedulerNotRunning,

    /// Scheduler already running
    SchedulerAlreadyRunning,

    /// Configuration error
    Config(String),

    /// Generic error
    Other(String),
}

Job Statistics

Getting Job Stats

let stats = scheduler.get_stats("my_job").await?;

println!("Job: {}", stats.name);
println!("Enabled: {}", stats.enabled);
println!("Executions: {}", stats.execution_count);

if let Some(last_run) = stats.last_run {
    println!("Last run: {}", last_run);
}

if let Some(next_run) = stats.next_run {
    println!("Next run: {}", next_run);
}

match stats.status {
    JobStatus::Scheduled => println!("Status: Waiting"),
    JobStatus::Running => println!("Status: Running"),
    JobStatus::Completed => println!("Status: Completed"),
    JobStatus::Failed(err) => println!("Status: Failed - {}", err),
}

Common Patterns

Database Cleanup Job

scheduler.add_job(
    "cleanup_old_records",
    "0 0 2 * * *", // 2 AM daily
    |ctx| Box::pin(async move {
        let db = get_database_connection().await?;

        let deleted = db.execute(
            "DELETE FROM sessions WHERE expires_at < NOW()"
        ).await?;

        println!("Deleted {} expired sessions", deleted);
        Ok(())
    })
)?;

Report Generation

scheduler.add_job(
    "daily_report",
    "0 0 8 * * MON-FRI", // 8 AM on weekdays
    |ctx| Box::pin(async move {
        let report = generate_daily_report().await?;
        send_email_report(report).await?;
        println!("Daily report sent");
        Ok(())
    })
)?;

Cache Warming

scheduler.add_job(
    "warm_cache",
    "0 */15 * * * *", // Every 15 minutes
    |ctx| Box::pin(async move {
        let cache = get_cache().await?;
        let data = fetch_frequently_accessed_data().await?;
        cache.set("popular_items", data, Some(Duration::from_secs(900))).await?;
        Ok(())
    })
)?;

API Rate Limit Reset

scheduler.add_job(
    "reset_rate_limits",
    "0 0 * * * *", // Every hour
    |ctx| Box::pin(async move {
        let limiter = get_rate_limiter().await?;
        limiter.reset_hourly_limits().await?;
        println!("Rate limits reset");
        Ok(())
    })
)?;

Health Check

scheduler.add_job(
    "health_check",
    "*/30 * * * * *", // Every 30 seconds
    |ctx| Box::pin(async move {
        let status = check_service_health().await?;

        if !status.is_healthy {
            send_alert("Service unhealthy").await?;
        }

        Ok(())
    })
)?;

Best Practices

1. Prevent Job Overlap

Jobs automatically prevent overlapping executions by default:

// This is enabled by default
job.prevent_overlap = true;

If a job is still running when its next scheduled time arrives, it will be skipped.

2. Use Appropriate Intervals

Don't schedule jobs too frequently:

// โŒ Bad: Too frequent, might cause performance issues
"* * * * * *" // Every second

// โœ… Good: Reasonable interval
"0 */5 * * * *" // Every 5 minutes

3. Handle Errors Gracefully

scheduler.add_job(
    "resilient_job",
    "0 * * * * *",
    |ctx| Box::pin(async move {
        let result = risky_operation().await;

        match result {
            Ok(data) => {
                process(data).await?;
                Ok(())
            }
            Err(e) => {
                // Log error but don't crash
                eprintln!("Job failed: {}", e);
                // Optionally return error to mark job as failed
                Err(CronError::ExecutionFailed(e.to_string()))
            }
        }
    })
)?;

4. Use Shared State Carefully

use std::sync::Arc;
use tokio::sync::RwLock;

let shared_state = Arc::new(RwLock::new(AppState::new()));
let state_clone = shared_state.clone();

scheduler.add_job(
    "state_job",
    "0 * * * * *",
    move |ctx| {
        let state = state_clone.clone();
        Box::pin(async move {
            let mut state = state.write().await;
            state.update().await?;
            Ok(())
        })
    }
)?;

5. Monitor Job Execution

// Periodically check job statistics
scheduler.add_job(
    "monitor",
    "0 */10 * * * *", // Every 10 minutes
    |ctx| Box::pin(async move {
        let stats = get_all_job_stats().await?;

        for stat in stats {
            if let JobStatus::Failed(err) = stat.status {
                send_alert(&format!("Job {} failed: {}", stat.name, err)).await?;
            }
        }

        Ok(())
    })
)?;

6. Graceful Shutdown

#[tokio::main]
async fn main() -> Result<(), CronError> {
    let mut scheduler = CronScheduler::new();

    // Add jobs...

    scheduler.start().await?;

    // Wait for shutdown signal
    tokio::signal::ctrl_c().await?;

    println!("Shutting down scheduler...");
    scheduler.stop().await?;

    // Wait for running jobs to complete
    tokio::time::sleep(Duration::from_secs(5)).await;

    println!("Shutdown complete");
    Ok(())
}

7. Use Job Metadata

// Jobs support metadata for additional information
let mut job = Job::new("backup", expression, handler);
job.set_metadata("environment", "production");
job.set_metadata("priority", "high");
job.set_metadata("owner", "ops-team");

Integration with Armature

Using with Dependency Injection

use armature_framework::prelude::*;
use armature_cron::*;

#[injectable]
struct CronService {
    scheduler: Arc<RwLock<CronScheduler>>,
}

impl CronService {
    pub fn new() -> Self {
        Self {
            scheduler: Arc::new(RwLock::new(CronScheduler::new())),
        }
    }

    pub async fn setup_jobs(&self) -> CronResult<()> {
        let mut scheduler = self.scheduler.write().await;

        scheduler.add_job(
            "cleanup",
            "0 0 * * * *",
            |ctx| Box::pin(async move {
                // Job logic
                Ok(())
            })
        )?;

        scheduler.start().await?;
        Ok(())
    }
}

#[module]
struct AppModule {
    providers: vec![CronService::provider()],
}

HTTP Endpoints for Job Management

#[controller("/api/cron")]
struct CronController {
    cron_service: CronService,
}

#[routes]
impl CronController {
    #[get("/jobs")]
    async fn list_jobs(&self) -> Json<Vec<String>> {
        let scheduler = self.cron_service.scheduler.read().await;
        Json(scheduler.list_jobs().await)
    }

    #[post("/jobs/:name/enable")]
    async fn enable_job(&self, #[param] name: String) -> Result<HttpResponse, Error> {
        let scheduler = self.cron_service.scheduler.read().await;
        scheduler.enable_job(&name).await
            .map_err(|e| Error::BadRequest(e.to_string()))?;
        Ok(HttpResponse::ok("Job enabled"))
    }

    #[post("/jobs/:name/disable")]
    async fn disable_job(&self, #[param] name: String) -> Result<HttpResponse, Error> {
        let scheduler = self.cron_service.scheduler.read().await;
        scheduler.disable_job(&name).await
            .map_err(|e| Error::BadRequest(e.to_string()))?;
        Ok(HttpResponse::ok("Job disabled"))
    }

    #[get("/jobs/:name/stats")]
    async fn get_stats(&self, #[param] name: String) -> Result<Json<JobStats>, Error> {
        let scheduler = self.cron_service.scheduler.read().await;
        let stats = scheduler.get_stats(&name).await
            .map_err(|e| Error::NotFound(e.to_string()))?;
        Ok(Json(stats))
    }
}

Summary

The Armature cron system provides:

  • โœ… Standard cron syntax for familiar scheduling
  • โœ… Async execution for non-blocking jobs
  • โœ… Job management with enable/disable/remove operations
  • โœ… Statistics for monitoring and debugging
  • โœ… Error handling with detailed error types
  • โœ… Overlap prevention to avoid concurrent executions
  • โœ… Integration with Armature's DI system

Perfect for:

  • Database maintenance
  • Report generation
  • Cache warming
  • Data synchronization
  • Health checks
  • Rate limit resets
  • Cleanup tasks
  • Scheduled notifications

For more examples, see examples/cron_jobs.rs.