Feasibility of implementing cronjob with Rust programming language
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 theTokyo
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