Skip to content

zhongwencool/ecron

Repository files navigation

Ecron GitHub Actions CodeCov Hex Tag License Hex Docs

Resilient, lightweight, efficient, cron-like job scheduling library for Erlang/Elixir.

EcronLogo

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 to unlimited.
  • By default, singleton is set to false, which means multiple instances of the same job can run concurrently. Set singleton to true 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

supervisor:child_spec/0

YourRegister = your_register,
Children = [
  #{
        id => YourRegister,
        start => {ecron, start_link, [YourRegister]},
        restart => permanent,
        shutdown => 1000,
        type => worker
    }
]

Elixir

supervisor:child_spec/1

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 to local (default), ecron uses calendar:local_time() to get the current datetime in your system's timezone
  • When time_zone is set to utc, 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 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.

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? {: .info}

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.