Feasibility of implementing cronjob with Rust programming language

Thanaphoom Babparn
6 min readApr 12, 2024

--

Hello everyone. In this article we’re going to talk about how you can write your own cron jobs by using Rust programming language. Basically, This is just a my pet project to find opportunities to write Rust.

Cronjob

Cron jobs (or “scheduled tasks”) are useful for many things. They allow you to automatically do things like:

  • Doing daily operations
  • Creating/writing reports daily, monthly.
  • Automate data back-ups.

There are so many use-cases for the cronjob. But how does it work?

Cron jobs is automated tasks that are scheduled to execute at regular intervals. It’s developed based on crontab on Linux.

The cron pattern is an important aspect of configuring automated operations with cron jobs. 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)
| | | | └───── month (1 - 12)
| | | └─────── day of month (1 - 31)
| | └───────── hour (0 - 23)
| └─────────── minute (0 - 59)
└───────────── second (optional, 0 - 59)

Example 1:

  • Cron Syntax: 0 15 10 * * MON-FRI
  • Meaning: This cron job will execute at 10:15 AM every Monday, Tuesday, Wednesday, Thursday, and Friday. (Everyday except Saturday and Sunday)

Example 2:

  • Cron Syntax: 0 */15 0 1 * *
  • Meaning: This cron job will execute every 15 minutes, at the start of every hour, on the 1 day of each month.

Here is my knowledge to give to you before we start working pet project.

GitHub repository

In this repository, I explored on 2 options
1. Using the cron — normal setup
2. Using the apalis — to running on tokio runtime

Option 1 — Setup by cron schedule

This option is mainly using below dependencies to setup (I use chrono-tz because of timezone setting)

[dependencies]
cron = "0.12.1"
chrono = "0.4"
chrono-tz = "0.9.0"

Most of the time, I start in really simple for testing if my code is working or not

    pub fn create_cronjob_with_schedule(cron_expression: &str, task: fn()) {
let schedule = Schedule::from_str(cron_expression)
.expect("Couldn't start the scheduler. Check the cron expression.");
loop {
let utc_now = Utc::now().naive_utc();
let jst_now = Tokyo.from_utc_datetime(&utc_now);
if let Some(next) = schedule.upcoming(jst_now.timezone()).take(1).next() {
let until_next = next - jst_now;
thread::sleep(until_next.to_std().unwrap());
println!("Running task at {}", jst_now.to_string());
task();
}
}
}

The code essentially creates a custom cron-like scheduler. It continuously checks the current time in Tokyo against a defined schedule. Whenever the current time matches a scheduled time, it executes your specified task.

The function takes two parameters:

  • cron_expression: A string slice (&str) representing the cron schedule.
  • task: A function pointer (fn()), specifying the task to be executed on schedule.

The cron expression is parsed into a Schedule object using Schedule::from_str(cron_expression). If the cron expression is invalid, the program will panic and print an error message.

Then infinite loop (loop), ensuring the task is executed repeatedly as per the cron schedule. Contain with time calculation.

  • utc_now: The current UTC time is obtained and converted to a naive datetime.
  • jst_now: This UTC time is then converted to Japan Standard Time (JST) using the Tokyo timezone.

The schedule.upcoming() method computes the next scheduled time to run the task, relative to jst_now. It then calculates the time duration until_next until this next scheduled time then put thread to sleep for the duration until_next, using thread::sleep, effectively pausing execution until it's time to run the scheduled task.

Once the sleep duration ends, it prints the current time (in JST) and executes the task() function.

Here my example once the basic integration has done

Option 2— Setup by apalis

This option is for the use-case to have worker that can customize to work with asynchronous runtime e.g. on backend service like Axum or Actix.

This need apalis dependency as a core to have worker that able to schedule, retry or attached tracing extension/layer for the cronjob.

[dependencies]
cron = "0.12.1"
chrono = "0.4"
apalis = { version = "0.5.1", features = ["cron", "retry", "async-std-comp"] }
tokio = { version = "1", features = ["full"] }
chrono-tz = "0.9.0"
apalis-cron = "0.5.1"

The example of worker configuration. You can see the different between option 1 and option 2.

    pub async fn create_cron_from_apalis(cron_expression: &str, cron_execution_service: CronExecutionService) {
let schedule = Schedule::from_str(cron_expression)
.expect("Couldn't start the scheduler. Check the cron expression.");
let worker = WorkerBuilder::new("worker")
.layer(RetryLayer::new(RetryPolicy::retries(3)))
.stream(CronStream::new(schedule).into_stream())
.data(cron_execution_service)
.build_fn(perform_task);
Monitor::<TokioExecutor>::new()
.register(worker)
.run()
.await
.unwrap();
}

async fn perform_task(job: CronArgument, svc: Data<CronExecutionService>) {
svc.execute(job);
}

create_cron_from_apalis, which sets up a cron job using a specified cron expression and an execution service.

It does so by building a worker with a retry policy, scheduling it with a stream derived from the cron expression. The associated perform_task function, also defined here, takes a cron job argument and a service, and executes the job using the service. This setup uses asynchronous execution to manage scheduled tasks effectively.

In order to integrating with apalis and able to execute perform_task, You need to implement impl From<DateTime<Utc>> to your own struct and organize to your intention. My example is naming it as CronArgument

    #[derive(Debug, Clone)]
pub struct CronArgument {
pub date_time: DateTime<Tz>,
}

impl From<DateTime<Utc>> for CronArgument {
fn from(t: DateTime<Utc>) -> Self {
CronArgument {
date_time: t.with_timezone(&Tz::Japan)
}
}
}

You can find more example from apalis here

Here is my simple result after integrating apalis to my project.

Send me the dad joke every 5 mins

Since this is pet project for me, and I don’t want to end at the Hello World tutorial. So I want to use ollama-rs to communicate with LLM on my laptop. Basically, Provide prompt and get response without any RAG technique or any chain.

In my case, I want to use Ollama on my laptop and for this article, I used mistral

I create helper function to hitting Ollama to help generate dad joke.

pub mod ollama_helper {
use ollama_rs::generation::completion::request::GenerationRequest;
use ollama_rs::Ollama;

pub async fn get_joke() -> Result<String, String> {
// By default it will connect to localhost:11434
let ollama = Ollama::default();
let model = "mistral:latest".to_string();
let prompt = "I need the most unique, outlandish dad joke you've got. The weirder, the better! Give me 1".to_string();
let ollama_result = ollama.generate(GenerationRequest::new(model, prompt)).await;
match ollama_result {
Ok(generation_response) => Ok(generation_response.response),
Err(e) => Err(e),
}
}
}

Here my all content libs module

Example 1 — With Schedule

cargo run --example ex1-with-schedule

Example 2— With apalis setup

cargo run --example ex2-with-apalis

Unfortunately, Seem the model response not so creative as I expected. I understand because I haven’t stored the historical response. That’s why the result keep duplicate like this.

Anyway, The purpose of this article is cronjob in Rust. 🙇‍♂️

Conclusions

In this short article, we’ve explored what cron jobs are, how they work behind the scenes, and how to leverage Rust features to create cron jobs.

Thank you for reading through this article. See you in the next blog post. 🙇‍♂️

Facebook: Thanaphoom Babparn
FB Page: TP Coder
LinkedIn: Thanaphoom Babparn
Website: TP Coder — Portfolio

--

--

Thanaphoom Babparn
Thanaphoom Babparn

Written by Thanaphoom Babparn

Software engineer who wanna improve himself and make an impact on the world.