When developing a web application in Elixir, you may occasionally need to schedule a task to run at regular intervals, like sending email notifications, cleaning up old records, or triggering a background process. While there are libraries like Quantum or Oban that make this task easier, sometimes you want a lightweight, custom solution without adding external dependencies.
In this post, we'll walk through how to create a simple cron-like job in an Elixir web app using built-in tools. We'll create a periodic worker that executes a task serially every minute, and you can easily customize it to your specific needs.
Why Not Use External Tools?
There are many great libraries available for scheduling jobs in Elixir, but sometimes it's overkill to introduce them,
especially when your needs are minimal or you're working in a resource-constrained environment. Building a solution
using just Elixir's GenServer
and
Process
modules provides you with full control, simplicity, and no
additional dependency overhead.
Setting Up the Periodic Worker
The core of our solution is a GenServer that schedules and runs tasks periodically. It executes a function, waits for a specified interval, and then repeats. This ensures that each task runs serially and won't overlap with the next one.
Let's dive into the implementation.
Step 1: Define the GenServer Module
Here's how we define our periodic worker using the GenServer
behavior. This worker will schedule a task to be executed
every minute (60 seconds).
1defmodule MyApp.PeriodicWorker do
2 use GenServer
3
4 @interval 60_000 # 1 minute interval (in milliseconds)
5
6 # Start the GenServer
7 def start_link(_) do
8 GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
9 end
10
11 @impl true
12 def init(:ok) do
13 # Start the first job immediately
14 schedule_work()
15 {:ok, %{}}
16 end
17
18 @impl true
19 def handle_info(:work, state) do
20 # Perform the task
21 perform_task()
22
23 # Schedule the next job after it finishes
24 schedule_work()
25
26 {:noreply, state}
27 end
28
29 defp schedule_work() do
30 # Schedule the next execution after the specified interval
31 Process.send_after(self(), :work, @interval)
32 end
33
34 defp perform_task() do
35 # Your logic here
36 IO.puts("Executing task at #{DateTime.utc_now()}")
37 # Add your function execution logic here, e.g., calling another module's function
38 end
39end
Step 2: Add the Worker to Your Supervision Tree
To ensure that your periodic worker starts when your application boots, you need to add it to your supervision tree.
Open your application.ex
file and include it as part of your app's children processes.
1def start(_type, _args) do
2 children = [
3 # Other children...
4 MyApp.PeriodicWorker
5 ]
6
7 opts = [strategy: :one_for_one, name: MyApp.Supervisor]
8 Supervisor.start_link(children, opts)
9end
This ensures that the worker starts automatically and is supervised. If it crashes, the supervisor will restart it, keeping your application robust and fault-tolerant.
Step 3: Customize the Task
In the perform_task/0
function, we currently just print the time, but you can replace this with your custom logic. For
example, you could:
- Send emails.
- Clean up outdated data.
- Ping external APIs.
- Any background task specific to your application.
How It Works
- GenServer Start: The GenServer is started when your application boots.
- Initial Task Execution: The
init/1
function triggers the first task immediately using theschedule_work/0
function. - Periodic Execution: After the task finishes, the worker schedules the next execution after a delay of 60,000
milliseconds (1 minute) using
Process.send_after/3
. - Serial Execution: Each task is executed serially. Once a task completes, the next one is scheduled. This ensures that tasks don't overlap, preventing race conditions or unnecessary load.
Why This Approach?
Using Elixir's built-in features provides several advantages:
- Lightweight: No need to add any external dependencies or tools.
- Full Control: You have complete control over the task scheduling and execution.
- Supervised: The worker is part of the supervision tree, ensuring that it's restarted if anything goes wrong.
- Easy to Customize: You can easily adjust the interval and customize the task logic to suit your needs.
Adjusting the Interval
If you need to change the frequency of execution, you can adjust the @interval
value:
1@interval 30_000 # 30 seconds interval
This flexibility allows you to fine-tune how often your task runs, whether it's every few seconds, minutes, or even hours.
Conclusion
Building a simple cron-like job in Elixir doesn't require complex tools or external libraries. With just a GenServer
and a bit of process scheduling, you can create a lightweight, serial task executor that meets your needs. This approach
is not only efficient but also leverages Elixir's powerful concurrency and supervision model, making it reliable and
easy to maintain.
For lightweight background tasks, this method is often all you need. Of course, if your needs grow more complex, or if you need more advanced job features like retries or distributed processing, you can always explore more feature-rich libraries. But for now, you've got a simple and powerful solution built entirely with Elixir.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.