Skip to content

Commit d1d7a17

Browse files
committed
Supervision Configuration for Cluster Managers
1 parent a1ca7c4 commit d1d7a17

9 files changed

Lines changed: 251 additions & 109 deletions

lib/quantum.ex

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,54 @@ defmodule Quantum do
1919
2020
## Configuration:
2121
22-
* `:timeout` - Sometimes, you may come across GenServer
23-
timeout errors esp. when you have too many jobs or high
24-
load. The default GenServer.call timeout is 5000.
22+
* `:clock_broadcaster_name` - GenServer name of clock broadcaster \\
23+
*(unstable, may break without major release until declared stable)*
24+
25+
* `:execution_broadcaster_name` - GenServer name of execution broadcaster \\
26+
*(unstable, may break without major release until declared stable)*
27+
28+
* `:executor_supervisor_name` - GenServer name of execution supervisor \\
29+
*(unstable, may break without major release until declared stable)*
30+
31+
* `:debug_logging` - Turn on debug logging
2532
2633
* `:jobs` - list of cron jobs to execute
2734
28-
* `:schedule` - Default schedule of new Job
35+
* `:job_broadcaster_name` - GenServer name of job broadcaster \\
36+
*(unstable, may break without major release until declared stable)*
37+
38+
* `:name` - GenServer name of scheduler \\
39+
*(unstable, may break without major release until declared stable)*
40+
41+
* `:node_selector_broadcaster_name` - GenServer name of node selector broadcaster \\
42+
*(unstable, may break without major release until declared stable)*
43+
44+
* `:overlap` - Default overlap of new Job
45+
46+
* `:otp_app` - Application where scheduler runs
2947
3048
* `:run_strategy` - Default Run Strategy of new Job
3149
32-
* `:overlap` - Default overlap of new Job,
50+
* `:schedule` - Default schedule of new Job
51+
52+
* `:storage` - Storage to use for persistence
53+
54+
* `:storage_name` - GenServer name of storage \\
55+
*(unstable, may break without major release until declared stable)*
56+
57+
* `:supervisor_module` - Module to supervise scheduler \\
58+
Can be overwritten to supervise processes differently (for example for clustering) \\
59+
*(unstable, may break without major release until declared stable)*
60+
61+
* `:task_registry_name` - GenServer name of task registry \\
62+
*(unstable, may break without major release until declared stable)*
63+
64+
* `:task_supervisor_name` - GenServer name of task supervisor \\
65+
*(unstable, may break without major release until declared stable)*
66+
67+
* `:timeout` - Sometimes, you may come across GenServer timeout errors
68+
esp. when you have too many jobs or high load. The default `GenServer.call/3`
69+
timeout is `5_000`.
3370
3471
* `:timezone` - Default timezone of new Job
3572
@@ -45,7 +82,6 @@ defmodule Quantum do
4582
@type t :: module
4683

4784
@defaults [
48-
cron: [],
4985
timeout: 5_000,
5086
schedule: nil,
5187
overlap: true,
@@ -56,8 +92,6 @@ defmodule Quantum do
5692
storage: Noop
5793
]
5894

59-
@optional_callbacks init: 1
60-
6195
# Returns the configuration stored in the `:otp_app` environment.
6296
@doc false
6397
@callback config(Keyword.t()) :: Keyword.t()
@@ -66,7 +100,7 @@ defmodule Quantum do
66100
Starts supervision and return `{:ok, pid}`
67101
or just `:ok` if nothing needs to be done.
68102
69-
Returns `{:error, {:already_started, pid}}` if the repo is already
103+
Returns `{:error, {:already_started, pid}}` if the scheduler is already
70104
started or `{:error, term}` in case anything else goes wrong.
71105
72106
## Options
@@ -136,16 +170,58 @@ defmodule Quantum do
136170

137171
@doc false
138172
# Retrieves only scheduler related configuration.
139-
def scheduler_config(scheduler, otp_app, custom) do
173+
def scheduler_config(opts, scheduler, otp_app) do
140174
@defaults
141175
|> Keyword.merge(Application.get_env(otp_app, scheduler, []))
142-
|> Keyword.merge(custom)
143-
|> Keyword.merge(otp_app: otp_app, scheduler: scheduler)
176+
|> Keyword.merge(opts)
177+
|> Keyword.put_new(:otp_app, otp_app)
178+
|> Keyword.put_new(:scheduler, scheduler)
179+
|> Keyword.put_new(:name, scheduler)
144180
|> update_in([:schedule], &Normalizer.normalize_schedule/1)
181+
|> Keyword.put_new(:task_supervisor_name, Module.concat(scheduler, TaskSupervisor))
182+
|> Keyword.put_new(:storage_name, Module.concat(scheduler, Storage))
183+
|> Keyword.put_new(:task_registry_name, Module.concat(scheduler, TaskRegistry))
184+
|> Keyword.put_new(:clock_broadcaster_name, Module.concat(scheduler, ClockBroadcaster))
185+
|> Keyword.put_new(:job_broadcaster_name, Module.concat(scheduler, JobBroadcaster))
186+
|> Keyword.put_new(
187+
:execution_broadcaster_name,
188+
Module.concat(scheduler, ExecutionBroadcaster)
189+
)
190+
|> Keyword.put_new(
191+
:node_selector_broadcaster_name,
192+
Module.concat(scheduler, NodeSelectorBroadcaster)
193+
)
194+
|> Keyword.put_new(:executor_supervisor_name, Module.concat(scheduler, ExecutorSupervisor))
195+
|> (fn config ->
196+
Keyword.update(config, :jobs, [], fn jobs ->
197+
jobs
198+
|> Enum.map(&Normalizer.normalize(scheduler.new_job(config), &1))
199+
|> remove_jobs_with_duplicate_names(scheduler)
200+
end)
201+
end).()
202+
|> Keyword.put_new(:supervisor_module, Quantum.Supervisor)
203+
|> Keyword.put_new(:name, Quantum.Supervisor)
204+
end
205+
206+
defp remove_jobs_with_duplicate_names(job_list, scheduler) do
207+
job_list
208+
|> Enum.reduce(%{}, fn %Job{name: name} = job, acc ->
209+
if Enum.member?(Map.keys(acc), name) do
210+
Logger.warn(
211+
"Job with name '#{name}' of scheduler '#{scheduler}' not started due to duplicate job name"
212+
)
213+
214+
acc
215+
else
216+
Map.put_new(acc, name, job)
217+
end
218+
end)
219+
|> Map.values()
145220
end
146221

147222
defmacro __using__(opts) do
148-
quote bind_quoted: [behaviour: __MODULE__, opts: opts, moduledoc: @moduledoc] do
223+
quote bind_quoted: [behaviour: __MODULE__, opts: opts, moduledoc: @moduledoc],
224+
location: :keep do
149225
@otp_app Keyword.fetch!(opts, :otp_app)
150226
@moduledoc moduledoc
151227
|> String.replace(~r/MyApp\.Scheduler/, Enum.join(Module.split(__MODULE__), "."))
@@ -155,27 +231,25 @@ defmodule Quantum do
155231

156232
@doc false
157233
@impl behaviour
158-
def config(custom \\ []) do
159-
Quantum.scheduler_config(__MODULE__, @otp_app, custom)
234+
def config(opts \\ []) do
235+
Quantum.scheduler_config(opts, __MODULE__, @otp_app)
160236
end
161237

162238
defp __job_broadcaster__ do
163-
__job_broadcaster__(
164-
config() |> Keyword.fetch!(:scheduler) |> Module.concat(JobBroadcaster),
165-
config()
166-
)
167-
end
168-
169-
defp __job_broadcaster__(job_broadcaster, configuration) do
170-
GenServer.whereis(job_broadcaster)
239+
config() |> Keyword.fetch!(:job_broadcaster_name)
171240
end
172241

173242
defp __timeout__, do: Keyword.fetch!(config(), :timeout)
174243

175244
@impl behaviour
176245
def start_link(opts \\ []) do
177-
opts = Keyword.put_new(opts, :name, __MODULE__)
178-
Quantum.Supervisor.start_link(__MODULE__, @otp_app, opts)
246+
opts = config(opts)
247+
Keyword.fetch!(opts, :supervisor_module).start_link(__MODULE__, opts)
248+
end
249+
250+
@impl behaviour
251+
def init(opts) do
252+
opts
179253
end
180254

181255
@impl behaviour
@@ -249,7 +323,7 @@ defmodule Quantum do
249323
%{unquote_splicing(spec)}
250324
end
251325

252-
defoverridable child_spec: 1
326+
defoverridable child_spec: 1, config: 0, config: 1, init: 1
253327
end
254328
end
255329
end

lib/quantum/clock_broadcaster.ex

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,22 @@ defmodule Quantum.ClockBroadcaster do
1111

1212
@spec start_link(opts :: StartOpts.t()) :: GenServer.on_start()
1313
def start_link(%StartOpts{name: name} = opts) do
14-
GenStage.start_link(
15-
__MODULE__,
14+
__MODULE__
15+
|> GenStage.start_link(
1616
struct!(InitOpts, Map.take(opts, [:start_time, :storage, :scheduler, :debug_logging])),
1717
name: name
1818
)
19+
|> case do
20+
{:ok, pid} ->
21+
{:ok, pid}
22+
23+
{:error, {:already_started, pid}} ->
24+
Process.monitor(pid)
25+
{:ok, pid}
26+
27+
{:error, _reason} = error ->
28+
error
29+
end
1930
end
2031

2132
@impl GenStage
@@ -59,6 +70,10 @@ defmodule Quantum.ClockBroadcaster do
5970
handle_tick(state)
6071
end
6172

73+
def handle_info(_message, state) do
74+
{:noreply, [], state}
75+
end
76+
6277
defp handle_tick(%State{remaining_demand: 0} = state) do
6378
{:noreply, [], state}
6479
end

lib/quantum/execution_broadcaster.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ defmodule Quantum.ExecutionBroadcaster do
3636
# Start Stage
3737
@spec start_link(StartOpts.t()) :: GenServer.on_start()
3838
def start_link(%StartOpts{name: name} = opts) do
39-
GenStage.start_link(
40-
__MODULE__,
39+
__MODULE__
40+
|> GenStage.start_link(
4141
struct!(
4242
InitOpts,
4343
Map.take(opts, [
@@ -50,6 +50,17 @@ defmodule Quantum.ExecutionBroadcaster do
5050
),
5151
name: name
5252
)
53+
|> case do
54+
{:ok, pid} ->
55+
{:ok, pid}
56+
57+
{:error, {:already_started, pid}} ->
58+
Process.monitor(pid)
59+
{:ok, pid}
60+
61+
{:error, _reason} = error ->
62+
error
63+
end
5364
end
5465

5566
@impl GenStage
@@ -148,6 +159,11 @@ defmodule Quantum.ExecutionBroadcaster do
148159
|> execute_events_to_fire(time)
149160
end
150161

162+
@impl GenStage
163+
def handle_info(_message, state) do
164+
{:noreply, [], state}
165+
end
166+
151167
defp initialize_jobs(%State{uninitialized_jobs: uninitialized_jobs} = state, time) do
152168
uninitialized_jobs
153169
|> Enum.reject(&match?(%Job{schedule: %CronExpression{reboot: true}}, &1))

lib/quantum/executor_supervisor.ex

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ defmodule Quantum.ExecutorSupervisor do
1111

1212
@spec start_link(StartOpts.t()) :: GenServer.on_start()
1313
def start_link(%StartOpts{name: name} = opts) do
14-
ConsumerSupervisor.start_link(
15-
__MODULE__,
14+
__MODULE__
15+
|> ConsumerSupervisor.start_link(
1616
struct!(
1717
InitOpts,
1818
Map.take(opts, [
@@ -24,6 +24,17 @@ defmodule Quantum.ExecutorSupervisor do
2424
),
2525
name: name
2626
)
27+
|> case do
28+
{:ok, pid} ->
29+
{:ok, pid}
30+
31+
{:error, {:already_started, pid}} ->
32+
Process.monitor(pid)
33+
{:ok, pid}
34+
35+
{:error, _reason} = error ->
36+
error
37+
end
2738
end
2839

2940
@impl ConsumerSupervisor

lib/quantum/job_broadcaster.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@ defmodule Quantum.JobBroadcaster do
1515
# Start Job Broadcaster
1616
@spec start_link(StartOpts.t()) :: GenServer.on_start()
1717
def start_link(%StartOpts{name: name} = opts) do
18-
GenStage.start_link(
19-
__MODULE__,
18+
__MODULE__
19+
|> GenStage.start_link(
2020
struct!(InitOpts, Map.take(opts, [:jobs, :storage, :scheduler, :debug_logging])),
2121
name: name
2222
)
23+
|> case do
24+
{:ok, pid} ->
25+
{:ok, pid}
26+
27+
{:error, {:already_started, pid}} ->
28+
Process.monitor(pid)
29+
{:ok, pid}
30+
31+
{:error, _reason} = error ->
32+
error
33+
end
2334
end
2435

2536
@impl GenStage
@@ -202,4 +213,9 @@ defmodule Quantum.JobBroadcaster do
202213

203214
def handle_call({:find_job, name}, _, %State{jobs: jobs} = state),
204215
do: {:reply, Map.get(jobs, name), [], state}
216+
217+
@impl GenStage
218+
def handle_info(_message, state) do
219+
{:noreply, [], state}
220+
end
205221
end

lib/quantum/node_selector_broadcaster.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ defmodule Quantum.NodeSelectorBroadcaster do
1818
# Start Stage
1919
@spec start_link(StartOpts.t()) :: GenServer.on_start()
2020
def start_link(%StartOpts{name: name} = opts) do
21-
GenStage.start_link(
22-
__MODULE__,
21+
__MODULE__
22+
|> GenStage.start_link(
2323
struct!(
2424
InitOpts,
2525
Map.take(opts, [
@@ -29,6 +29,17 @@ defmodule Quantum.NodeSelectorBroadcaster do
2929
),
3030
name: name
3131
)
32+
|> case do
33+
{:ok, pid} ->
34+
{:ok, pid}
35+
36+
{:error, {:already_started, pid}} ->
37+
Process.monitor(pid)
38+
{:ok, pid}
39+
40+
{:error, _reason} = error ->
41+
error
42+
end
3243
end
3344

3445
@impl GenStage
@@ -54,6 +65,11 @@ defmodule Quantum.NodeSelectorBroadcaster do
5465
end), state}
5566
end
5667

68+
@impl GenStage
69+
def handle_info(_message, state) do
70+
{:noreply, [], state}
71+
end
72+
5773
defp select_nodes(%Job{run_strategy: run_strategy} = job, task_supervisor) do
5874
run_strategy
5975
|> NodeList.nodes(job)

lib/quantum/normalizer.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ defmodule Quantum.Normalizer do
2525
#
2626
# * `base` - Empty `Quantum.Job`
2727
# * `job` - The Job To Normalize
28-
@spec normalize(Job.t(), config_full_notation | config_short_notation) :: Job.t() | no_return
28+
@spec normalize(Job.t(), config_full_notation | config_short_notation | Job.t()) ::
29+
Job.t() | no_return
2930
def normalize(base, job)
3031

3132
def normalize(%Job{} = base, job) when is_list(job) do
@@ -50,6 +51,10 @@ defmodule Quantum.Normalizer do
5051
normalize_options(base, %{schedule: schedule, task: task})
5152
end
5253

54+
def normalize(%Job{} = _base, %Job{} = job) do
55+
job
56+
end
57+
5358
@spec normalize_options(Job.t(), map) :: Job.t()
5459
defp normalize_options(job, options) do
5560
Enum.reduce(options, job, &normalize_job_option/2)

0 commit comments

Comments
 (0)