If you've ever needed to automate a task or ensure that an important job gets done on schedule, you've probably used a cronjob. Simply put, a cronjob is a tool that allows you to schedule tasks to run automatically at a predetermined time or interval. Whether it's sending out a daily report or backing up a database, cronjobs are a convenient way to automate repetitive or time-consuming tasks.

Recently, I was working on a side project where I wanted to explore the benefits and trade-offs of using multiple languages in a system. I decided to create a cronjob micro-service using Elixir, a functional programming language that is well-suited for building scalable and fault-tolerant systems.

In this blog post, I'll document my experience building the service, and share some of the benefits and challenges I encountered along the way. If you're interested in using Elixir to create your own cronjob service, or if you're just curious about what's involved, I hope you'll find this post helpful.

Why Elixir for a Cronjob Service?

When it came to choosing a language for my cronjob service, I knew I wanted something that was fast, scalable, and fault-tolerant. Elixir checked all of those boxes and then some.

For those unfamiliar with Elixir, it is a functional programming language that runs on the Erlang virtual machine. One of the key benefits of Elixir is its support for concurrency, which allows it to easily handle multiple tasks concurrently. This makes it well-suited for building scalable and fault-tolerant systems.

In addition to its good performance and concurrency support, I was also drawn to Elixir's functional nature. Elixir encourages a functional programming style, which can make it easier to reason about code and write tests. All of these factors made Elixir an appealing choice for my cronjob service.

Using Phoenix for the Web Interface

To build the web interface for my cronjob service, I decided to use the Phoenix framework. Phoenix is a popular web framework for Elixir that makes it easy to build scalable and reliable web applications. It offers a variety of features that made it a good fit for my cronjob service, including support for web sockets, channels, and live view.

One of the key benefits of Phoenix is its use of the actor model for concurrency. In Phoenix, each web request is handled by its own Elixir process, which makes it easy to scale the application by adding more processes. This makes Phoenix well-suited for building a cronjob service, which may need to handle a large number of concurrent tasks.

Overall Architecture

The overall architecture of my cronjob service is designed to be scalable and fault-tolerant. It is composed of multiple Elixir processes that communicate with each other using the actor model. Each process is responsible for a specific task, such as scheduling a job to run or executing a job.

To ensure that the service can recover from failures, I used Elixir's built-in process supervision to monitor the health of the service. If any process fails, the supervisor will restart it, ensuring that the service stays up and running.

The Role of the Database

In my cronjob service, I used a database to store information about the tasks that are scheduled to run and the status of those tasks. This made it easy to track the progress of the service and ensure that tasks were being run as expected.

Integrating a database with an Elixir-based service can sometimes be a challenge, but I found that Elixir's Ecto library made it relatively straightforward. Ecto is a database library for Elixir that provides a simple interface for querying and updating a database.

Setting up the Development Environment

Before I could start building my cronjob service, I needed to set up a development environment. This involved installing Elixir, the Phoenix framework, and any other dependencies that were needed.

If you're new to Elixir, the first step is to install the Elixir runtime and build tools. You can find instructions for installing Elixir on the Elixir website. Once Elixir is installed, you'll also need to install the Phoenix framework. You can do this by running the following command:

mix archive.install hex phx_new

Next, you'll need to set up a database for your cronjob service. I chose to use PostgreSQL, but you could use any database that is supported by Elixir's Ecto library. Once you have a database set up, you'll need to configure your development environment to use it. This typically involves creating a database and a user, and then updating your Phoenix configuration to use the correct database credentials.

Defining the Tasks

Once I had my development environment set up, I was ready to start defining the tasks that my cronjob service would run. I used Elixir's built-in scheduling functions, such as cron/4, to specify the schedule for each task. For example, if I wanted to run a task every hour, I would use the following code:

cron("0 * * * *", MyApp.TaskScheduler, :run_task, [])

In this example, the cron/4 function takes four arguments: a cron expression, a module, a function, and a list of arguments. The cron expression specifies the schedule for the task, and the module and function specify the code that should be run when the task is triggered.

Implementing the Tasks

Once I had defined the tasks that my cronjob service would run, I needed to implement the code that would actually perform the work. This involved writing Elixir functions that would be called by the cron/4 function when the tasks were triggered.

For example, let's say I had defined a task to send a daily report by email. The implementation of this task might look something like this:

defmodule MyApp.TaskScheduler do
  def run_task do
    # Generate the report
    report = generate_report()

    # Send the report by email
    send_email(report)
  end

  defp generate_report do
    # code to generate the report goes here
  end

  defp send_email(report) do
    # code to send the email goes here
  end
end

In this example, the run_task/0 function is the entry point for the task. It calls the generate_report/0 and send_email/1 functions to perform the work of generating and sending the report.

Configuring the Cronjob Service

Once I had implemented the tasks that my cronjob service would run, I needed to configure the service to run on a predetermined schedule. To do this, I used the Phoenix framework to set up routes and controllers for the service.

For example, let's say I wanted to create a web interface for my cronjob service that would allow users to view and manage the tasks that were scheduled to run.

To create a web interface for my cronjob service, I used the Phoenix framework to set up routes and controllers. For example, I might create a route like this:

scope "/tasks", MyApp do
  pipe_through :api
  resources "/", TaskController
end

This route would allow users to access the tasks resource at the /tasks URL. I could then create a TaskController to handle requests to this resource.

To make it easy for users to view and manage the tasks that were scheduled to run, I used Phoenix's live view feature. Live view allows you to build real-time, interactive interfaces with minimal coding. For example, I might create a live view like this:

defmodule MyApp.TaskLive do
  use Phoenix.LiveView
  
  def mount(_params, _session, socket) do
    tasks = fetch_tasks()
    {:ok, assign(socket, tasks: tasks)}
  end
  
  def render(assigns) do
    # code to render the live view goes here
  end
  
  def handle_event("add_task", %{"name" => name, "schedule" => schedule}, socket) do
    # code to handle the "add_task" event goes here
  end
  
  defp fetch_tasks do
    # code to fetch the tasks from the database goes here
  end
end

In this example, the mount/3 function is called when the live view is first rendered. It fetches the tasks from the database and assigns them to the tasks variable. The render/1 function is then called to render the live view, and the handle_event/3 function is called to handle events that are sent from the client (such as an "add_task" event).

Using live view made it easy for me to create a real-time, interactive interface for my cronjob service. Users could view and manage the tasks that were scheduled to run, and they could see the changes in real time as they were made.


External communication with the service

Here is an example of how I used a message queue (in this case, RabbitMQ) to communicate with the cronjob service:

First, I needed to set up a RabbitMQ server and install the amqp library, which is an Elixir client library for RabbitMQ. You can do this by adding the following dependencies to your mix.exs file:

defp deps do
  [
    {:amqp, "~> 3.0"}
  ]
end

Next, I needed to create a connection to the RabbitMQ server and set up a channel for sending and receiving messages. We can do this in our application's startup code:

def start(_type, _args) do
  # Connect to the RabbitMQ server
  {:ok, conn} = AMQP.Connection.open(...)

  # Open a channel
  {:ok, chan} = AMQP.Channel.open(conn)

  # Set up queues and exchanges
  AMQP.Queue.declare(chan, "tasks", durable: true)
  AMQP.Exchange.direct(chan, "tasks", durable: true)
  AMQP.Queue.bind(chan, "tasks", "tasks", "")

  # Start the task scheduler process
  TaskScheduler.start_link(chan)
  
    # Start the web server
  {:ok, _pid} = Phoenix.Server.start_link(...)
end

With the connection and channel set up, we can start using RabbitMQ to send and receive messages. Here is an example of how one might use it in the TaskScheduler process:

defmodule TaskScheduler do
  use GenServer

  def start_link(chan) do
    GenServer.start_link(__MODULE__, chan, name: __MODULE__)
  end

  def init(chan) do
    # Set up the queue and exchange
    {:ok, _queue} = AMQP.Queue.declare(chan, "", exclusive: true)
    AMQP.Queue.bind(chan, "", "tasks", "")

    # Set up a consumer to receive messages from the queue
    AMQP.Basic.consume(chan, "", fn(payload, _metadata, _ack) ->
      # Parse the message and schedule the task
      {:ok, %{"name" => name, "schedule" => schedule}} = Jason.decode(payload)
      schedule_task(name, schedule)

      # Acknowledge the message
      AMQP.Basic.ack(chan, _ack)
    end)

    # Return the initial state
    {:ok, chan}
  end

  def schedule_task(name, schedule) do
    # Code to schedule the task goes here
  end
end

This TaskScheduler process uses RabbitMQ to set up a consumer that listens for messages on the "tasks" exchange. When a message is received, it parses the message and calls the schedule_task/2 function to schedule the task.

To send a message to the cronjob service, you can use the AMQP.Basic.publish/4 function. For example:

def create_task(name, schedule) do
  # Connect to the RabbitMQ server
  {:ok, conn} = AMQP.Connection.open(...)

  # Open a channel
  {:ok, chan} = AMQP.Channel.open(conn)

  # Encode the message
  message = Jason.encode(%{"name" => name, "schedule" => schedule})

  
  # Publish the message to the "tasks" exchange
  AMQP.Basic.publish(chan, "tasks", "", message)
  
  # Close the channel and connection
  AMQP.Channel.close(chan)
  AMQP.Connection.close(conn)
end

Conclusion

Creating a cronjob micro-service using Elixir was a fun and interesting project. I enjoyed the process of setting up a development environment, defining and implementing tasks, and configuring the service to run on a predetermined schedule.

One of the biggest challenges I faced was getting used to the syntax and concepts of Elixir, which was new to me. However, once I got the hang of it, I found that Elixir was a powerful and expressive language that made it easy to build the cronjob service.

If you're interested in creating your own cronjob service using Elixir, I recommend checking out the following resources:

I hope this blog post has been helpful and gives you an idea of what's involved in creating a cronjob service using Elixir. If you have any questions or comments, I'd love to hear from you!

If you find anything wrong, or anything that needs correction, please feel free to leave a comment and let me know and I will make sure to check it out and address it.

Ciao 👋🏾


References

1. https://github.com/quantum-elixir/quantum-core

2. https://www.phoenixframework.org/

3. https://blog.kalvad.com/write-your-own-cron-with-with-elixir/

4. https://wrgoldstein.github.io/2017/02/20/phoenix-rabbitmq.html