Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple days scheduling feature #26

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions lib/cronex/every.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ defmodule Cronex.Every do

`frequency` supports the following values: `:minute`, `:hour`, `:day`, `:month`, `:year`, `:monday`, `:tuesday`, `:wednesday`, `:thursday`, `:friday`, `:saturday`, `:sunday`

`job` must be a list with the following structure: `[do: block]`, where `block` is the code refering to a specific job
`job` must be a list with the following structure: `[do: block]`, where `block` is the code refering to a specific job

## Example

every :day do
# Daily task here
# Daily task here
end

every :month do
# Monthly task here
# Monthly task here
end
"""
defmacro every(frequency, [do: block] = _job)
Expand Down Expand Up @@ -55,18 +55,18 @@ defmodule Cronex.Every do

`interval` must be an integer representing the interval of frequencies that should exist between each job run

`at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job
`at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job

`job` must be a list with the following structure: `[do: block]`, where `block` is the code corresponding to a specific job

## Example

every :day, at: "10:00" do
# Daily task at 10:00 here
# Daily task at 10:00 here
end

every :monday, at: "12:00" do
# Monday task at 12:00 here
# Monday task at 12:00 here
end

every 2, :day do
Expand All @@ -76,6 +76,10 @@ defmodule Cronex.Every do
every 3, :week do
# Every 3 weeks task
end

every [:sunday, :monday], at: "14:00" do
# Sunday and Monday task at 14:00 here
end
"""
defmacro every(arg1, [at: time] = _arg2, [do: block] = _job)
when is_atom(arg1) and is_bitstring(time) do
Expand Down Expand Up @@ -115,6 +119,26 @@ defmodule Cronex.Every do
end
end

defmacro every(arg1, [at: time], [do: block] = _job)
when is_list(arg1) and is_bitstring(time) do
days = Enum.join(arg1, "_")
job_name = String.to_atom("job_every_#{days}_at_#{time}")

quote do
@jobs unquote(job_name)

@doc false
def unquote(job_name)() do
Cronex.Job.new(
unquote(arg1),
unquote(time),
fn -> unquote(block) end
)
|> Cronex.Job.validate!()
end
end
end

@doc """
`Cronex.Every.every/4` macro is used as a simple interface to add a job to the `Cronex.Table`.

Expand All @@ -124,7 +148,7 @@ defmodule Cronex.Every do

`frequency` supports the following values: `:minute`, `:hour`, `:day`, `:month`

`at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job
`at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job

`job` must be a list with the following structure: `[do: block]`, where `block` is the code corresponding to a specific job

Expand Down
16 changes: 14 additions & 2 deletions lib/cronex/job.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ defmodule Cronex.Job do
|> Map.put(:task, task)
end

def new(arg1, arg2, task)
when is_list(arg1) and is_bitstring(arg2) and is_function(task) do
%Cronex.Job{}
|> Map.put(:frequency, parse_regular_frequency(arg1, arg2))
|> Map.put(:task, task)
end

@doc """
Creates a `%Job{}` with the given interval, frequency, time and task.

Expand Down Expand Up @@ -85,15 +92,15 @@ defmodule Cronex.Job do
# TODO Process.alive? only works for local processes, improve this to support several nodes

# Is time to run
# Job process is dead or non existing
# Job process is dead or non existing
is_time(job.frequency) and (job.pid == nil or !Process.alive?(job.pid))
end

defp raise_invalid_frequency_error do
raise ArgumentError, """
An invalid frequency was given when creating a job.

Check the docs to see the accepted frequency arguments.
Check the docs to see the accepted frequency arguments.
"""
end

Expand Down Expand Up @@ -130,6 +137,11 @@ defmodule Cronex.Job do
interval.(current_date_time().day - 1) == 0
end

# Every days of week job, check time and list of days of the week
defp is_time({minute, hour, :*, :*, days_of_week}) when is_list(days_of_week) do
Enum.any?(days_of_week, &is_time({minute, hour, :*, :*, &1}))
end

# Every week job, check time and day of the week
defp is_time({minute, hour, :*, :*, day_of_week}) do
current_date_time().minute == minute and current_date_time().hour == hour and
Expand Down
14 changes: 13 additions & 1 deletion lib/cronex/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ defmodule Cronex.Parser do
iex> Cronex.Parser.parse_regular_frequency(:wednesday, "12:00")
{0, 12, :*, :*, 3}

iex> Cronex.Parser.parse_regular_frequency([:friday, :saturday])
{0, 0, :*, :*, [5,6]}

iex> Cronex.Parser.parse_regular_frequency(:non_existing_day)
:invalid

Expand Down Expand Up @@ -56,6 +59,15 @@ defmodule Cronex.Parser do
day_of_week = Enum.find_index(@days_of_week, &(&1 == frequency)) + 1
{minute, hour, :*, :*, day_of_week}

is_list(frequency) and Enum.all?(frequency, &Enum.member?(@days_of_week, &1)) ->
days_of_week =
frequency
|> Enum.map(fn freq ->
Enum.find_index(@days_of_week, &(&1 == freq)) + 1
end)

{minute, hour, :*, :*, days_of_week}

true ->
:invalid
end
Expand All @@ -64,7 +76,7 @@ defmodule Cronex.Parser do
@doc """
Parses a given `interval`, `frequency` and `time` to a tuple.

`interval` is a function wich receives one argument and returns the remainder of the division of that argument by the given `interval`
`interval` is a function wich receives one argument and returns the remainder of the division of that argument by the given `interval`

## Example

Expand Down
6 changes: 4 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
%{"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}}
%{
"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], [], "hexpm", "0fdcd651f9689e81cda24c8e5d06947c5aca69dbd8ce3d836b02bcd0c6004592"},
"ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "6bf36498c4c67fdbe6d4ad73a112098cbcc09b147b859219b023fc2636729bf6"},
}
26 changes: 26 additions & 0 deletions test/cronex/job_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ defmodule Cronex.JobTest do
assert job == Job.validate!(job)
end

test "returns the given job if frequency contains a list of week days" do
task = fn -> :ok end
job = Job.new([:sunday, :monday], "12:00", task)

assert job == Job.validate!(job)
end

test "raises invalid frequency error when a job with an invalid frequency is given" do
task = fn -> :ok end
job = Job.new(:invalid_frequency, task)
Expand Down Expand Up @@ -200,4 +207,23 @@ defmodule Cronex.JobTest do
Test.DateTime.set(month: 1, day: 2, hour: 0, minute: 0)
assert false == Cronex.Job.can_run?(job)
end

test "can_run?/1 with a list of week days job" do
task = fn -> :ok end
job = Job.new([:sunday, :monday], "12:00", task)

# day_of_week == 0 (or 7)
Test.DateTime.set(year: 2021, month: 6, day: 13, hour: 12, minute: 0)
assert true == Cronex.Job.can_run?(job)

# day_of_week == 1
Test.DateTime.set(day: 14)
assert true == Cronex.Job.can_run?(job)

# days_of_week in 2..6
Enum.each(15..19, fn day ->
Test.DateTime.set(day: day)
assert false == Cronex.Job.can_run?(job)
end)
end
end
10 changes: 10 additions & 0 deletions test/cronex/parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ defmodule Cronex.ParserTest do
assert 0 == interval_fn.(8)
assert 2 == interval_fn.(2)
end

test "parse_regular_frequency/2" do
assert {0, 0, :*, :*, day_frequency} = Cronex.Parser.parse_regular_frequency(:monday, "00:00")
assert 1 == day_frequency

assert {30, 10, :*, :*, days_frequency} =
Cronex.Parser.parse_regular_frequency([:friday, :saturday], "10:30")

assert [5, 6] == days_frequency
end
end
22 changes: 21 additions & 1 deletion test/cronex/scheduler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ defmodule Cronex.SchedulerTest do
send(test_process(), {:ok, :every_friday})
end

every [:monday, :tuesday], at: "14:00" do
send(test_process(), {:ok, :every_monday_and_tuesday})
end

every 2, :hour do
send(test_process(), {:ok, :every_2_hour})
end
Expand All @@ -46,7 +50,7 @@ defmodule Cronex.SchedulerTest do

test "loads jobs from TestScheduler" do
assert %{0 => %Job{}, 1 => %Job{}, 2 => %Job{}} = Cronex.Table.get_jobs(TestScheduler.table())
assert 5 == Cronex.Table.get_jobs(TestScheduler.table()) |> map_size
assert 6 == Cronex.Table.get_jobs(TestScheduler.table()) |> map_size
end

test "TestScheduler starts table and task supervisor" do
Expand Down Expand Up @@ -79,6 +83,22 @@ defmodule Cronex.SchedulerTest do
refute_receive {:ok, :every_friday}, @timeout
end

test "every monday and tuesday job runs on the expected time" do
# day_of_week == 1
Test.DateTime.set(year: 2021, month: 6, day: 14, hour: 14, minute: 0)
assert_receive {:ok, :every_monday_and_tuesday}, @timeout

Test.DateTime.set(hour: 15)
refute_receive {:ok, :every_monday_and_tuesday}, @timeout

# day_of_week == 2
Test.DateTime.set(day: 15, hour: 14, minute: 0)
assert_receive {:ok, :every_monday_and_tuesday}, @timeout

Test.DateTime.set(hour: 15)
refute_receive {:ok, :every_monday_and_tuesday}, @timeout
end

test "every 2 hour job runs on the expected time" do
Test.DateTime.set(hour: 0, minute: 0)
assert_receive {:ok, :every_2_hour}, @timeout
Expand Down