View Source
Ecron
Resilient, lightweight, efficient, cron-like job scheduling library for Erlang/Elixir.
Ecron supports both cron-style and interval-based job scheduling, focusing on resiliency, correctness, and lightweight with comprehensive testing via PropTest.
Use Case
Ecron's precise scheduling is perfect for:
- Security:
0 3 * * 0
Rotate API keys and dynamic credentials automatically every Sunday at 3 AM - Flash Sale:
0 8 * * *
Launch flash sales with precision at 8 AM - Analytics:
0 9 * * 1
Sending comprehensive weekly reports every Monday at 9 AM - Disk Protection:
30 23 * * *
Compress and archive old logs at 23:30 daily - Data Cleanup:
0 1 1 * *
Pruning inactive users on the first day of each month - Data Backup:
0 2 * * *
Create reliable Mnesia database backups every day at 2 AM
Setup
Erlang
%% rebar.config
{deps, [{ecron, "~> 1.1.0"}]}
Elixir
# mix.exs
def deps do
[{:ecron, "~> 1.1.0"}]
end
Configuration Usage
Erlang
Configure ecron in your sys.config
file with job specifications:
%% sys.config
[
{ecron, [
{local_jobs, [
%% {JobName, CrontabSpec, {M, F, A}}
%% {JobName, CrontabSpec, {M, F, A}, PropListOpts}
%% CrontabSpec
%% 1. "Minute Hour DayOfMonth Month DayOfWeek"
%% 2. "Second Minute Hour DayOfMonth Month DayOfWeek"
%% 3. @yearly | @annually | @monthly | @weekly | @daily | @midnight | @hourly
%% 4. @every 1h2m3s
{basic, "*/15 * * * *", {io, format, ["Runs on 0, 15, 30, 45 minutes~n"]}},
{sec_in_spec, "0 0 1-6/2,18 * * *", {io, format, ["Runs on 1,3,6,18 o'clock:~n"]}},
{hourly, "@hourly", {io, format, ["Runs every(0-23) o'clock~n"]}},
{interval, "@every 30m", {io, format, ["Runs every 30 minutes"]}},
{limit_time, "*/15 * * * *", {io, format, ["Runs 0, 15, 30, 45 minutes after 8:20am~n"]}, [{start_time, {8,20,0}}, {end_time, {23, 59, 59}}]},
{limit_count, "@every 1m", {io, format, ["Runs 10 times"]}, [{max_count, 10}]},
{limit_concurrency, "@minutely", {timer, sleep, [61000]}, [{singleton, true}]},
{limit_runtime_ms, "@every 1m", {timer, sleep, [2000]}, [{max_runtime_ms, 1000}]}
]}
}
].
Elixir
Configure ecron in your config.exs
file with job specifications:
# config/config.exs
config :ecron,
local_jobs: [
# {job_name, crontab_spec, {module, function, args}}
# {job_name, crontab_spec, {module, function, args}, PropListOpts}
# CrontabSpec formats:
# 1. "Minute Hour DayOfMonth Month DayOfWeek"
# 2. "Second Minute Hour DayOfMonth Month DayOfWeek"
# 3. @yearly | @annually | @monthly | @weekly | @daily | @midnight | @hourly
# 4. @every 1h2m3s
{:basic, "*/15 * * * *", {IO, :puts, ["Runs on 0, 15, 30, 45 minutes"]}},
{:sec_in_spec, "0 0 1-6/2,18 * * *", {IO, :puts, ["Runs on 1,3,6,18 o'clock:"]}},
{:hourly, "@hourly", {IO, :puts, ["Runs every(0-23) o'clock"]}},
{:interval, "@every 30m", {IO, :puts, ["Runs every 30 minutes"]}},
{:limit_time, "*/15 * * * *", {IO, :puts, ["Runs 0, 15, 30, 45 minutes after 8:20am"]}, [start_time: {8,20,0}, end_time: {23, 59, 59}]},
{:limit_count, "@every 1m", {IO, :puts, ["Runs 10 times"]}, [max_count: 10]},
{:limit_concurrency, "@minutely", {Process, :sleep, [61000]}, [singleton: true]},
{:limit_runtime_ms, "@every 1m", {Process, :sleep, [2000]}, [max_runtime_ms: 1000]}
]
- When a job reaches its
max_count
limit, it will be automatically removed. By default,max_count
is set tounlimited
. - By default,
singleton
is set tofalse
, which means multiple instances of the same job can run concurrently. Setsingleton
totrue
to ensure only one instance runs at a time.
For all PropListOpts, refer to the documentation for ecron:create/4
.
Runtime Usage
Besides loading jobs from config files at startup, you can add jobs from your code.
Erlang
JobName = every_4am_job,
MFA = {io, format, ["Run at 04:00 every day.\n"]},
Options = #{max_runtime_ms => 1000},
ecron:create(JobName, "0 4 * * *", MFA, Options).
Statistic = ecron:statistic(JobName),
ecron:delete(JobName),
Statistic.
Elixir
job_name = :every_4am_job
mfa = {IO, :puts, ["Run at 04:00 every day.\n"]}
options = %{max_runtime_ms: 1000}
{:ok, ^job_name} = :ecron.create(job_name, "0 4 * * *", mfa, options)
statistic = :ecron.statistic(job_name)
:ecron.delete(job_name)
statistic
Multi Register
For most applications, the above two methods are enough. However, Ecron offers a more flexible way to manage job lifecycles.
For example, when applications A and B need separate cron jobs, you can create a dedicated register for each. This ensures jobs are removed when their parent application stops.
Erlang
{ok, _}= ecron:start_link(YourRegister),
ecron:create(YourRegister, JobName, Spec, MFA, Options),
ecron:delete(YourRegister, JobName).
Elixir
{:ok, _} = :ecron.start_link(YourRegister)
:ecron.create(YourRegister, job_name, spec, mfa, options)
:ecron.delete(YourRegister, job_name)
Alternatively, use a supervisor:
Erlang
YourRegister = your_register,
Children = [
#{
id => YourRegister,
start => {ecron, start_link, [YourRegister]},
restart => permanent,
shutdown => 1000,
type => worker
}
]
Elixir
children = [
worker(:ecron, [YourRegister], restart: :permanent)
]
After setup, use ecron:create/4
and ecron:delete/2
to manage your jobs.
Time Functions
Ecron can manage recurring jobs. It also supports one-time tasks and time-based message delivery.
ecron:send_after/3
Create a one-time timer that sends a message, just like erlang:send_after/3
but triggered with a crontab spec.
Erlang
ecron:send_after("*/4 * * * * *", self(), hello_world),
receive Msg -> io:format("receive:~s~n", [Msg]) after 5000 -> timeout end.
Elixir
:ecron.send_after("*/4 * * * * *", self(), :hello_world)
receive do msg -> IO.puts("receive: #{msg}") after 5000 -> :timeout end
Sends a message to a process repeatedly based on a crontab schedule.
ecron:send_interval/3
Create a repeating timer that sends a message, just like timer:send_interval/3
but triggered with a crontab spec.
Erlang
ecron:send_interval("*/4 * * * * *", self(), hello_world),
Loop = fun(Loop) ->
receive
Msg ->
io:format("[~p] receive: ~s~n", [erlang:time(), Msg]),
Loop(Loop)
after
5000 -> timeout
end
end,
Loop(Loop).
Elixir
:ecron.send_interval("*/4 * * * * *", self(), :hello_world)
loop = fn loop ->
receive do
msg ->
IO.puts("[#{Time.utc_now()}] receive: #{msg}")
loop.(loop)
after
5000 -> :timeout
end
end
loop.(loop)
Time Zone
Erlang
Configure ecron in your sys.config
file with timezone:
%% sys.config
[
{ecron, [
{time_zone, local} %% local or utc
]}
].
Elixir
# config/config.exs
config :ecron,
time_zone: :local # :local or :utc
- When
time_zone
is set tolocal
(default), ecron uses calendar:local_time() to get the current datetime in your system's timezone - When
time_zone
is set toutc
, ecron uses calendar:universal_time() to get the current datetime in UTC timezone
Troubleshooting
Ecron provides functions to assist with debugging at runtime:
ecron:statistic/x ecron:parse_spec/2
Telemetry
Ecron uses Telemetry for instrumentation and logging. Telemetry is a metrics and instrumentation library for Erlang and Elixir applications that is based on publishing events through a common interface and attaching handlers to handle those events. For more information about the library itself, see its README.
Ecron logs all events by default.
Events
Event | measurements map key | metadata map key | log level | |
success | run_microsecond,run_result,action_at | name,mfa | notice | |
activate | action_at | name,mfa | notice | |
deactivate | action_at | name | notice | |
delete | action_at | name | notice | |
crashed | run_microsecond,run_result,action_at | name,mfa | error | |
skipped | job_last_pid,reason,action_at | name,mfa | error | |
aborted | run_microsecond,action_at | name,mfa | error | |
global,up | quorum_size,good_nodes,bad_nodes,action_at | self(node) | alert | |
global,down | quorum_size,good_nodes,bad_nodes,action_at | self(node) | alert |
For all failed events, refer to the documentation for ecron:statistic/2
.
You can enable or disable logging via log
configuration.
Erlang
%% sys.config
[
{ecron, [
%% none | all | alert | error | notice
{log_level, all}
]}
].
Elixir
# config/config.exs
config :ecron,
# :none | :all | :alert | :error
log_level: :all
- all: Captures all events.
- none: Captures no events.
- alert: Captures global up/down events.
- error: Captures crashed, skipped, aborted, and global up/down events.
How?
Use logger:set_module_level(ecron_telemetry_logger, Log) to override the primary log level of Logger for log events originating from the ecron_telemetry_logger module. This means that even if you set the primary log level to error, but the module log level is set to all, all ecron logs will be captured.
Writing your own handler
If you want custom logging control, you can create your own event handler.
See src/ecron_telemetry_logger.erl
as a reference implementation.
Contributing
To run property-based tests, common tests, and generate a coverage report with verbose output.
$ rebar3 do proper -c, ct -c, cover -v
It takes about 10-15 minutes.
License
Ecron is released under the Apache-2.0 license. See the license file.