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.
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
%% rebar.config
{deps, [{ecron, "~> 1.1.0"}]}
# mix.exs
def deps do
[{:ecron, "~> 1.1.0"}]
end
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}]}
]}
}
].
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
.
Besides loading jobs from config files at startup, you can add jobs from your code.
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.
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
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.
{ok, _}= ecron:start_link(YourRegister),
ecron:create(YourRegister, JobName, Spec, MFA, Options),
ecron:delete(YourRegister, JobName).
{:ok, _} = :ecron.start_link(YourRegister)
:ecron.create(YourRegister, job_name, spec, mfa, options)
:ecron.delete(YourRegister, job_name)
Alternatively, use a supervisor:
YourRegister = your_register,
Children = [
#{
id => YourRegister,
start => {ecron, start_link, [YourRegister]},
restart => permanent,
shutdown => 1000,
type => worker
}
]
children = [
worker(:ecron, [YourRegister], restart: :permanent)
]
After setup, use ecron:create/4
and ecron:delete/2
to manage your jobs.
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.
ecron:send_after("*/4 * * * * *", self(), hello_world),
receive Msg -> io:format("receive:~s~n", [Msg]) after 5000 -> timeout end.
: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.
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).
: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)
Configure ecron in your sys.config
file with timezone:
%% sys.config
[
{ecron, [
{time_zone, local} %% local or utc
]}
].
# 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
Ecron provides functions to assist with debugging at runtime:
ecron:statistic/x ecron:parse_spec/2
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.
Event | Measurements Map Keys | Metadata Map Keys | 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.
%% sys.config
[
{ecron, [
%% none | all | alert | error | notice
{log_level, all}
]}
].
# 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.
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.
If you want custom logging control, you can create your own event handler.
See src/ecron_telemetry_logger.erl
as a reference implementation.
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.
Ecron is released under the Apache-2.0 license. See the license file.