Run Cron Jobs With Rust

April 04, 2024
Written by
Elijah Asaolu
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Run Cron Jobs with Rust

Consider never having to manually backup your database again or automatically sending birthday reminders to customers. This is the power of cron jobs: scheduled tasks that execute in the background.

In this article, we’ll go over how to get started with running cron jobs supercharged with Rust's efficiency and reliability. We’ll explore different examples, such as automating SMS broadcasts and database backups. And we’ll also go over when Rust might or might not be favorable for cron-related tasks.

Prerequisites

To follow along with this tutorial, the following prerequisites are required:

What are Cron jobs?

Cron jobs are automated tasks that are scheduled to execute at regular intervals. They are frequently used for a number of tasks, including backups, database updates, email delivery, and much more, and do not require someone to start them each time.

At the heart of cron jobs is the cron daemon. Think of it as a behind-the-scenes worker that constantly checks a special list, known as a cron table or crontab, to see what tasks need to be done and when. Each task in the list contains a unique schedule (a cron expression) that instructs the daemon when to run it, followed by the specific action to be taken.

The way cron works is pretty straightforward, but also smart. Every minute, this worker checks the list to find any tasks that are scheduled for the current time. If it finds any, it starts those tasks right away in a separate process. This setup makes sure tasks happen exactly when they're supposed to, whether that's every day at a certain time, or more unusual patterns, like only on certain days or months.

The Cron syntax

The cron pattern is an important aspect of configuring automated operations with cron jobs. It is smartly designed to allow you to schedule tasks for practically any time you can think of, from every minute to precise times on specific days or months. This pattern comprises six fields, each separated by a space, and each one represents a distinct time unit.

* * * * * *
  | | | | | |
  | | | | | └─── day of week (0 - 7) (Sunday to Saturday; 7 is also Sunday)
  | | | | └───── month (1 - 12)
  | | | └─────── day of month (1 - 31)
  | | └───────── hour (0 - 23)
  | └─────────── minute (0 - 59)
  └───────────── second (optional, 0 - 59)

Each field can take specific values that dictate when your task will run, giving you the flexibility to automate tasks exactly when you need them to happen. For example, to set up a job that runs every three days, we can use a numerical value in the day-of-month field, like so:

0 0 */3 * *

This expression means the job will run at midnight (0 0) every 3 days (*/3), every month (*), and on every day of the week (*).

Consider an even more advanced cron expression designed for detailed scheduling:

0 0 * Jan-Apr Mon,Wed,Fri

This setup schedules a job to run at midnight (0 0) on Monday, Wednesday, and Friday (Mon,Wed,Fri) from January through April (Jan-Apr).

Implement a Cron job in Rust

The Rust ecosystem is abundant with libraries for various tasks, including scheduling cron jobs. Among these, the cron crate stands out as the most suitable choice for several reasons. It is straightforward to integrate, well-documented, and receives steady updates, making it more reliable compared to other libraries.

To get started, create a new Rust project by running the following command in your terminal.

cargo new rust_cron_job
cd rust_cron_job

These commands create a new Rust project named rust_cron_job in a new directory with the same name, containing the basic Rust binary project structure.

Next, add the cron library to your project by including it in your Cargo.toml file under the [dependencies] section:

[dependencies]
cron = "0.12.1"
chrono = "0.4"

This step informs Cargo that your project depends on the cron and chronocrates; the latter is a prerequisite for handling dates and times in Rust.

Now that you have your Rust project set up and the necessary dependencies declared, you can begin writing your first cron job. Edit the main.rs file in your project's src directory to include the following Rust code:

use chrono::Utc;
use cron::Schedule;
use std::str::FromStr;
use std::thread;

fn main() {
    let expression = "0/5 * * * * *";
    let schedule = Schedule::from_str(expression).expect("Failed to parse CRON expression");

    for datetime in schedule.upcoming(Utc).take(1) {
        let now = Utc::now();
        let until = datetime - now;
        thread::sleep(until.to_std().unwrap());
        println!("Hello, world!");
    }
}

In this example, the task is set to run every five seconds as defined by the cron expression "0/5 * * * * *". The code converts this expression into a schedule and calculates the exact time until the next execution. It then pauses execution with thread::sleep(), until this time is reached, ensuring precise timing. Once the wait is over, it executes the task, which in this case is printing "Hello, world!".

Execute your cron job by running the following command in the terminal:

cargo run

This command compiles and runs your Rust application, and you should see "Hello, World!" printed to your terminal five seconds after running the code, demonstrating the cron job in action.

However, it's crucial to understand that the task will not continue to execute every five seconds, instead stopping after the first execution. This behavior is due to Rust's design, which, unlike languages like JavaScript, does not include an intrinsic event loop. Rust requires explicit management for continuous task execution or looping. For a task to repeat at regular intervals within a single run of a Rust application, you must explicitly implement looping logic.

To modify our example for continuous execution, we can introduce a loop that perpetually checks for the next scheduled time and executes the task accordingly. Update src/main.rs with the code below to implement this.

use chrono::{Local, Utc};
use cron::Schedule;
use std::str::FromStr;
use std::thread;

fn main() {
    let expression = "0/5 * * * * *";
    let schedule = Schedule::from_str(expression).expect("Failed to parse CRON expression");

    loop {
        let now = Utc::now();
        if let Some(next) = schedule.upcoming(Utc).take(1).next() {
            let until_next = next - now;
            thread::sleep(until_next.to_std().unwrap());
            println!(
                "Running every 5 seconds. Current time: {}",
                Local::now().format("%Y-%m-%d %H:%M:%S")
            );
        }
    }
}

In this continuous version, the application remains active, entering a loop that frequently checks the schedule to see if it's time to execute the task again. If so, the application sleeps until the next execution time, and then prints the current time, demonstrating the task running every 5 seconds, as shown below.

Running every 5 seconds. Current time: 2024-03-12 18:22:35
Running every 5 seconds. Current time: 2024-03-12 18:22:40
Running every 5 seconds. Current time: 2024-03-12 18:22:45
Running every 5 seconds. Current time: 2024-03-12 18:22:50
Running every 5 seconds. Current time: 2024-03-12 18:22:55
Running every 5 seconds. Current time: 2024-03-12 18:23:00
Running every 5 seconds. Current time: 2024-03-12 18:23:05
Running every 5 seconds. Current time: 2024-03-12 18:23:10
...

Now, let's look at some interesting cron job use cases.

Automate the SMS broadcast in Rust with Cron and Twilio

One impressive use of cron jobs is to automate customized message delivery, such as delivering birthday greetings to customers or reminders for specific events. Cron jobs are ideal for these use cases, since they can execute scheduled tasks consistently and automatically.

Let's look at how to automate birthday SMS messages using the Twilio Messaging API . To begin, navigate to your Twilio Console dashboard and obtain your Account SID and Auth Token, as shown in the screenshot below.

Next, we need to install the necessary crates to make requests to the Twilio API. In the dependencies section of  Cargo.toml, add entries for Reqwest, Tokio, Dotenv, and Serde by adding the dependencies below after the existing ones.

reqwest = "0.11"
tokio = { version = "1", features = ["full"] }
dotenv = "0.15"
serde = { version = "1.0", features = ["derive"] }

These crates are essential for making asynchronous requests, handling environment variables, and serializing data.

Next, create a new .env file in your project root directory and paste the following content into it, replacing the placeholders with your actual Twilio credentials.

TWILIO_ACCOUNT_SID="<your_account_sid>"
TWILIO_AUTH_TOKEN="<your_auth_token>"
TWILIO_NUMBER="<your_twilio_number>"

Now, let's set up our Rust application. We'll create an array of users with their names, dates of birth, and phone numbers. The application will check daily to see if any user has a birthday and, if so, send them a birthday greeting via SMS using the Twilio Programmable Messaging API. 

Open src/main.rs and replace its content with the code below.

use chrono::{Datelike, Duration, Local, NaiveDate, Timelike};
use dotenv::dotenv;
use reqwest::Client;
use serde::Serialize;
use std::env;

#[derive(Debug)]
struct User {
    name: String,
    dob: NaiveDate, // Format: YYYY-MM-DD
    phone_number: String,
}

#[derive(Serialize)]
struct TwilioMessage {
    From: String,
    To: String,
    Body: String,
}

async fn send_sms(
    user: &User,
    from: &str,
    account_sid: &str,
    auth_token: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let url = format!(
        "https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json",
        account_sid
    );

    let msg = TwilioMessage {
        From: from.to_string(),
        To: user.phone_number.clone(),
        Body: format!("Happy Birthday, {}!", user.name),
    };

    let response = client
        .post(&url)
        .basic_auth(account_sid, Some(auth_token))
        .form(&msg)
        .send()
        .await?;

    if !response.status().is_success() {
        let err_body = response.text().await?;
        println!("Failed to send message. Error: {}", err_body);
        return Err(Box::new(std::io::Error::new(
            std::io::ErrorKind::Other,
            "SMS send failed",
        )));
    } else {
        println!("Message sent successfully!");
    }
    Ok(())
}

#[tokio::main]
async fn main() {
    dotenv().ok();
    let account_sid = env::var("TWILIO_ACCOUNT_SID").unwrap();
    let auth_token = env::var("TWILIO_AUTH_TOKEN").unwrap();
    let twilio_number = env::var("TWILIO_NUMBER").unwrap();

    let today_date = Local::now().date_naive();

    let users = vec![
        User {
            name: "Alice".into(),
            dob: NaiveDate::from_ymd_opt(1990, 3, 29).expect("Invalid date"),
            phone_number: "+12345678901".into(),
        },
        User {
            name: "Bob".into(),
            dob: today_date,
            phone_number: "+10987654321".into(),
        },
    ];

    loop {
        let today = Local::now().naive_local();
        for user in users.iter() {
            if user.dob.month() == today.month() && user.dob.day() == today.day() {
                if let Err(e) = send_sms(user, &twilio_number, &account_sid, &auth_token).await {
                    println!("Error sending SMS: {:?}", e);
                }
            }
        }

        let now = Local::now();
        // Adjusting to avoid direct use of deprecated .date()
        let next_day_start = (now + Duration::try_days(1).unwrap())
            .with_hour(0)
            .unwrap()
            .with_minute(0)
            .unwrap()
            .with_second(0)
            .unwrap()
            .with_nanosecond(0)
            .unwrap()
            + Duration::try_seconds(1).unwrap(); // Direct use of seconds, assuming no overflow concern

        let duration_until_next_day = next_day_start.signed_duration_since(now);
        let tokio_sleep_duration = tokio::time::Duration::from_secs(
            duration_until_next_day.num_seconds().try_into().unwrap(),
        );
        tokio::time::sleep(tokio_sleep_duration).await;
    }
}

The implementation above involves defining a User struct with fields for name, date of birth (DOB), and phone number. Using the reqwest crate, we asynchronously send an HTTP POST request to Twilio's messaging API endpoint, authenticating with our Account SID and Auth Token and using our Twilio phone number as the sender.

Each user's DOB is checked against the current date, and a personalized "Happy Birthday" message is sent to users celebrating their birthday.

To test the functionality, start your application with the following command:

cargo run

For demonstration purposes, the second user’s dob has been updated to programmatically be the present date. To proceed, replace this user's phone number with your mobile phone number to see message delivery in action. You should receive a birthday SMS on your phone. A successful message delivery can also be verified through Twilio's logs on your dashboard, by looking under Monitor > Logs > Messaging.

Automate MySQL database backups

Imagine needing to routinely back up a MySQL database, such as a users database, every day at 2 AM. With Rust and the cron library, setting up such an automated process is straightforward.

Let’s get started by creating a new user database and filling it out with dummy data. Open your terminal and run the series of commands below.

mysql -u root -p
CREATE DATABASE Users;
USE Users;
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    phone VARCHAR(20)
);
INSERT INTO users (name, phone) 
VALUES ('John Doe', '123-456-7890'), ('Jane Doe', '098-765-4321');

The command above logs into MySQL using the root user and prompts for the password due to the -p option. Next, we create a new database named Users and select this database for subsequent operations. We also created a new table named Users within the Users database. Finally, we insert two rows into the Users table, each row representing a user with a specified name and phone number.

To proceed, open the default src/main.rs and paste the following code.

use chrono::Local;
use cron::Schedule;
use std::process::Command;
use std::str::FromStr;
use std::thread;
use std::time::Duration;

fn main() {
    let expression = "0 0 2 * * *"; // Every day at 2 AM
    let schedule = Schedule::from_str(expression).expect("Failed to parse CRON expression");

    loop {
        let now = Local::now();
        if let Some(datetime) = schedule.upcoming(Local).take(1).next() {
            let until = datetime - now;
            thread::sleep(until.to_std().unwrap());

            let date_str = Local::now().format("%Y_%m_%d").to_string();
            let backup_file_name = format!("backup_{}.sql", date_str);

            // Replace your username and password accordingly
            let command = format!(
                "mysqldump -u <YOUR_DB_USERNAME> -p'<YOUR_DB_PASSWORD>' Users > {}",
                backup_file_name
            );

            match Command::new("sh").arg("-c").arg(command).output() {
                Ok(output) => {
                    if output.status.success() {
                        println!("Backup created successfully: {}", backup_file_name);
                    } else {
                        let error_message = String::from_utf8_lossy(&output.stderr);
                        eprintln!("Error creating backup: {}", error_message);
                    }
                }
                Err(e) => eprintln!("Failed to execute command: {}", e),
            }
        }
    }
}

Then, replace the placeholders with your database credentials.

In the code above, the cron library is utilized to set the schedule for backups for database Users  as "0 0 2 * * *", indicating a daily backup at 2 AM. We are leveraging the mysqldump utility, a command-line utility provided by MySQL for backing up databases​​, to perform the database dump, directing the output to a file named backup_${date}.sql. ${date} is dynamically generated based on the current date.

For the sake of this tutorial and to observe the backup process in action without waiting until 2 AM, change the cron expression to "0 * * * * *" to configure the job to execute at the start of every minute:

let expression = "0 * * * * *";

Next, start the application by running the command below.

cargo run

You should see the database backup happening in real time, every minute.

Rust for Cron Jobs?

Rust, with its focus on performance, safety, and concurrency, offers compelling reasons to use it for developing applications that include cron job functionality. However, its design nature also means it may not be the first choice in all scenarios. Let's explore when Rust could be particularly favorable for implementing cron jobs and situations where its design might not be as advantageous.

When Rust Is favorable for Cron jobs

Rust is an excellent choice for cron jobs that involve CPU-intensive operations or require efficient resource management (such as processing large datasets, performing complex calculations, or handling high-performance computing tasks) due to its emphasis on zero-cost abstractions and memory safety without garbage collection. Rust can optimize hardware capabilities without the overhead associated with languages that rely on runtime environments or virtual machines.

Additionally, Rust provides powerful abstractions to safely manage concurrent operations for applications that benefit from or require concurrent or parallel processing within their cron jobs (e.g., executing multiple data processing tasks in parallel). Its ownership and borrowing system, along with the ecosystem's async-await syntax, make it easier to write efficient, error-free concurrent code.

When Rust might not be as favorable

Applications that require the continuous execution and dynamic scheduling of a range of activities based on external triggers may benefit from event-driven execution environments such as Node.js.

While Rust can manage these requirements, particularly with the Tokio runtime for async I/O, the difficulty of operating a long-running, event-driven program may be better managed in languages built specifically for such patterns. 

Furthermore, Rust's boilerplate and compile-time checks may slow down the initial development process when compared to dynamically typed languages for projects requiring rapid development and easy prototyping.

Conclusion

In this tutorial, we’ve explored what cron jobs are, how they work behind the scenes, and how to leverage Rust features to create cron jobs. We also explored various examples, such as automating SMS delivery and database backup. And finally, we covered when and why Rust should be considered for cron jobs. 

All the code used in this tutorial can also be found here. Thanks for reading!

Elijah Asaolu is a technical writer and software engineer. He frequently enjoys writing technical articles to share his skills and experience with other developers.