%%% @copyright Erlware, LLC. All Rights Reserved. %%% %%% This file is provided to you under the BSD License; you may not use %%% this file except in compliance with the License. %%%------------------------------------------------------------------- %%% @doc %%% Simple one for one supervisor for ecd_chron jobs. -module(ecrn_cron_sup). -behaviour(supervisor). %% API -export([start_link/0, add_job/2, add_job/3, all_jobs/0, terminate/1]). %% Supervisor callbacks -export([init/1]). -define(SERVER, ?MODULE). %%%=================================================================== %%% API functions %%%=================================================================== -spec start_link() -> {ok, pid()} | ignore | {error, Error::term()}. start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). %% @doc %% Add a cron job to be supervised -spec add_job(erlcron:job_ref(), erlcron:job()) -> erlcron:job_ref(). add_job(JobRef, Job) -> add_job(JobRef, Job, #{}). %% @doc %% Add a cron job to be supervised -spec add_job(erlcron:job_ref(), erlcron:job(), erlcron:cron_opts()) -> erlcron:job_ref() | ignored | already_started | {error, term()}. add_job(JobRef, Job = #{}, CronOpts) when is_map(CronOpts) -> {JobSpec, JobOpts} = parse_job(Job), add_job2(JobRef, JobSpec, check_opts(JobRef, maps:merge(CronOpts, JobOpts))); add_job(JobRef, Job = {_, _Task}, CronOpts) when is_map(CronOpts) -> add_job2(JobRef, Job, check_opts(JobRef, CronOpts)); add_job(JobRef, {When, Task, JobOpts}, CronOpts) when is_map(JobOpts) -> add_job2(JobRef, {When, Task}, check_opts(JobRef, maps:merge(CronOpts, JobOpts))). add_job2(JobRef, Job = {_, Task}, Opts) -> case check_host(Opts) of true -> check_task(JobRef, Task), case supervisor:start_child(?SERVER, [JobRef, Job, Opts]) of {ok, _} -> JobRef; {error, {already_started, _}} -> already_started; Other -> Other end; false -> ignored end. get_opt(Opt, Map) -> case maps:take(Opt, Map) of {V, Map1} -> {V, Map1}; error -> erlang:error({missing_job_option, Opt, Map}) end. parse_job(Job) -> {When, Opts1} = get_opt(interval, Job), {Fun, Opts2} = get_opt(execute, Opts1), {{When, Fun}, Opts2}. %% @doc %% Get a list of all active jobs -spec all_jobs() -> [pid()]. all_jobs() -> [P || {_,P,_,_} <- supervisor:which_children(?SERVER)]. %% @doc %% Terminate a job terminate(Pid) when is_pid(Pid) -> supervisor:terminate_child(?SERVER, Pid). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== %% @private init([]) -> SupFlags = #{ strategy => simple_one_for_one, intensity => application:get_env(erlcron, sup_job_intensity, 3), period => application:get_env(erlcron, sup_job_period, 10) }, AChild = #{id => ecrn_agent, start => {ecrn_agent, start_link, []}, restart => transient, shutdown => brutal_kill, type => worker, modules => [ecrn_agent]}, {ok, {SupFlags, [AChild]}}. %%%=================================================================== %%% Internal functions %%%=================================================================== check_opts(JobRef, Map) -> maps:foreach(fun (hostnames, L) when is_list(L) -> ok; (on_job_start, MF) when tuple_size(MF)==2; is_function(MF, 1) -> ok; (on_job_end, MF) when tuple_size(MF)==2; is_function(MF, 2) -> ok; (id, ID) when is_atom(ID); is_binary(ID); is_reference(ID) -> ok; (K, V) -> Info = if is_function(V) -> [Name, Arity, Mod, Env0] = [element(2, erlang:fun_info(V, I)) || I <- [name, arity, module, env]], Fun = lists:flatten(io_lib:format("~w/~w", [Name, Arity])), case Env0 of [T|_] when is_tuple(T) -> {Mod, element(1,T), Fun}; %% {Module, {Line, Pos}, Fun} _ -> {Mod, Fun} end; true -> V end, erlang:error({invalid_option_value, JobRef, {K, Info}}) end, Map), Map. check_host(Opts) -> case maps:find(hostnames, Opts) of {ok, Hosts} when is_list(Hosts) -> {ok, Host} = inet:gethostname(), lists:member(Host, [to_list(H) || H <- Hosts]); error -> true end. check_task(JobRef, Task) when is_tuple(Task), (tuple_size(Task)==2 orelse tuple_size(Task)==3) -> M = element(1, Task), case code:ensure_loaded(M) of {module, M} -> ok; {error, Err1} -> erlang:error({module_not_loaded, JobRef, Task, Err1}) end, check_exists(JobRef, Task); check_task(_, Task) when is_function(Task, 0) -> ok; check_task(_, Task) when is_function(Task, 2) -> ok; check_task(JobRef, Task) -> erlang:error({invalid_job_task, JobRef, Task}). check_exists(JobRef, {M,F}) -> check_exists2(JobRef, {M,F,undefined}); check_exists(JobRef, {_,_,A} = MFA) when is_list(A) -> check_exists2(JobRef, MFA). check_exists2(JobRef, {M,F,A} = Task) -> case erlang:module_loaded(M) of false -> case code:ensure_loaded(M) of {module, M} -> ok; {error, Err1} -> erlang:error({module_not_loaded, JobRef, Task, Err1}) end; true -> ok end, case A of undefined -> check_arity(JobRef, M, F, [0,2]); _ when is_list(A) -> check_arity(JobRef, M, F, [length(A)]) end. check_arity(JobRef, M, F, Lengths) -> {module, M} == code:ensure_loaded(M) orelse erlang:error({job_task_module_not_loaded, JobRef, M}), lists:any(fun(Arity) -> erlang:function_exported(M,F,Arity) end, Lengths) orelse erlang:error({wrong_arity_of_job_task, JobRef, report_arity(M,F,Lengths)}). report_arity(M, F, [A]) -> lists:flatten(io_lib:format("~w:~w/~w", [M, F, A])); report_arity(M, F, A) when is_list(A) -> Arities = string:join([integer_to_list(I) || I <- A], ","), lists:flatten(io_lib:format("~w:~w/[~s]", [M, F, Arities])). to_list(H) when is_binary(H) -> binary_to_list(H); to_list(H) when is_list(H) -> H.