From 4a299af8972297e606714e7efc81184b410754cf Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 3 Nov 2020 20:36:18 +0800 Subject: [PATCH 01/11] check job's spec before init --- include/ecron.hrl | 4 +- src/ecron_monitor.erl | 3 +- src/ecron_sup.erl | 44 ++++++++++++---------- src/ecron_tick.erl | 75 ++++++++++++++++++-------------------- test/prop_ecron_server.erl | 2 +- test/prop_ecron_status.erl | 8 ++-- 6 files changed, 69 insertions(+), 67 deletions(-) diff --git a/include/ecron.hrl b/include/ecron.hrl index 18fc122..02cffb1 100644 --- a/include/ecron.hrl +++ b/include/ecron.hrl @@ -1,6 +1,6 @@ -define(MONITOR_WORKER, ecron_monitor). --define(Job, ecron_local_jobs). --define(GlobalJob, ecron_global_jobs). +-define(LocalJob, ecron_local). +-define(GlobalJob, ecron_global). -define(Ecron, ecron). -define(MAX_TIMEOUT, 4294967). %% (16#ffffffff div 1000) 49.71 days. diff --git a/src/ecron_monitor.erl b/src/ecron_monitor.erl index a53fef3..9a1bb34 100644 --- a/src/ecron_monitor.erl +++ b/src/ecron_monitor.erl @@ -5,6 +5,7 @@ -export([start_link/2]). -export([health/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). +-include("ecron.hrl"). start_link(Name, Jobs) -> gen_server:start_link(Name, ?MODULE, [Jobs], []). @@ -39,7 +40,7 @@ handle_info(_Msg, State) -> {noreply, State}. health() -> - case erlang:whereis(ecron) of + case erlang:whereis(?LocalJob) of undefined -> {error, node()}; _ -> {ok, node()} end. diff --git a/src/ecron_sup.erl b/src/ecron_sup.erl index f1d241f..d956b94 100644 --- a/src/ecron_sup.erl +++ b/src/ecron_sup.erl @@ -13,10 +13,11 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). start_global(Measurements) -> + GlobalJobs = application:get_env(?Ecron, global_jobs, []), case supervisor:start_child(?MODULE, #{ id => ?GLOBAL_WORKER, - start => {ecron_tick, start_link, [{global, ?Ecron}]}, + start => {ecron_tick, start_link, [{global, ?GlobalJob}, GlobalJobs]}, restart => temporary, shutdown => 1000, type => worker, @@ -36,7 +37,9 @@ stop_global(Measurements) -> end. init([]) -> - ?Job = ets:new(?Job, [named_table, set, public, {keypos, 2}]), + LocalJobs = application:get_env(?Ecron, local_jobs, []), + GlobalJobs = application:get_env(?Ecron, global_jobs, []), + ?LocalJob = ets:new(?LocalJob, [named_table, set, public, {keypos, 2}]), SupFlags = #{ strategy => one_for_one, intensity => 100, @@ -44,26 +47,27 @@ init([]) -> }, Local = #{ id => ?LOCAL_WORKER, - start => {?LOCAL_WORKER, start_link, [{local, ?Ecron}]}, + start => {?LOCAL_WORKER, start_link, [{local, ?LocalJob}, LocalJobs]}, restart => permanent, shutdown => 1000, type => worker, modules => [?LOCAL_WORKER] }, - Monitor = monitor_worker(), - {ok, {SupFlags, [Local | Monitor]}}. - -monitor_worker() -> - Jobs = application:get_env(?Ecron, global_jobs, []), - monitor_worker(Jobs). - -monitor_worker([]) -> []; -monitor_worker(Jobs) -> - [#{ - id => ?MONITOR_WORKER, - start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, Jobs]}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [?MONITOR_WORKER] - }]. + Worker = + case GlobalJobs of + [] -> [Local]; + _ -> + ?GlobalJob = ets:new(?GlobalJob, [named_table, set, public, {keypos, 2}]), + [ + Local, + #{ + id => ?MONITOR_WORKER, + start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [?MONITOR_WORKER] + } + ] + end, + {ok, {SupFlags, Worker}}. diff --git a/src/ecron_tick.erl b/src/ecron_tick.erl index eaf88ca..57c39d3 100644 --- a/src/ecron_tick.erl +++ b/src/ecron_tick.erl @@ -7,10 +7,10 @@ -export([reload/0]). -export([predict_datetime/2, parse_crontab/2]). --export([start_link/1, handle_call/3, handle_info/2, init/1, handle_cast/2]). +-export([start_link/2, handle_call/3, handle_info/2, init/1, handle_cast/2]). -export([spawn_mfa/3, clear/0]). --record(state, {time_zone, max_timeout, timer_tab, job_tab = ?Job}). +-record(state, {time_zone, max_timeout, timer_tab, job_tab = ?LocalJob}). -record(job, {name, status = activate, job, opts = [], ok = 0, failed = 0, link = undefined, result = [], run_microsecond = []}). -record(timer, {key, name, cur_count = 0, singleton, type, spec, mfa, link, @@ -21,16 +21,16 @@ -define(day_of_week(Y, M, D), (case calendar:day_of_the_week(Y, M, D) of 7 -> 0; D1 -> D1 end)). -define(MatchSpec(Name), [{#timer{name = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Name}}]}]). -add(Job, Options) -> gen_server:call(?Ecron, {add, Job, Options}, infinity). -delete(Name) -> gen_server:call(?Ecron, {delete, Name}, infinity). -activate(Name) -> gen_server:call(?Ecron, {activate, Name}, infinity). -deactivate(Name) -> gen_server:call(?Ecron, {deactivate, Name}, infinity). -get_next_schedule_time(Name) -> gen_server:call(?Ecron, {next_schedule_time, Name}, infinity). -clear() -> gen_server:call(?Ecron, clear, infinity). -reload() -> gen_server:cast(?Ecron, reload). +add(Job, Options) -> gen_server:call(?LocalJob, {add, Job, Options}, infinity). +delete(Name) -> gen_server:call(?LocalJob, {delete, Name}, infinity). +activate(Name) -> gen_server:call(?LocalJob, {activate, Name}, infinity). +deactivate(Name) -> gen_server:call(?LocalJob, {deactivate, Name}, infinity). +get_next_schedule_time(Name) -> gen_server:call(?LocalJob, {next_schedule_time, Name}, infinity). +clear() -> gen_server:call(?LocalJob, clear, infinity). +reload() -> gen_server:cast(?LocalJob, reload). statistic(Name) -> - case ets:lookup(?Job, Name) of + case ets:lookup(?LocalJob, Name) of [Job] -> {ok, job_to_statistic(Job)}; [] -> try @@ -41,7 +41,7 @@ statistic(Name) -> end. statistic() -> - Local = ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], ?Job), + Local = ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], ?LocalJob), Global = try gen_server:call({global, ?Ecron}, statistic) @@ -55,14 +55,18 @@ predict_datetime(Job, Num) -> Now = current_millisecond(), predict_datetime(activate, Job, unlimited, unlimited, Num, TZ, Now). -start_link(Name) -> gen_server:start_link(Name, ?MODULE, [Name], []). - -init([{Type, _}]) -> +start_link(Name, JobSpec) -> + case parse_crontab(JobSpec, []) of + {ok, Jobs} -> gen_server:start_link(Name, ?MODULE, [Name, Jobs], []); + {stop, Reason} -> {error, Reason} + end. + +init([{Type, JobTab}, Jobs]) -> erlang:process_flag(trap_exit, true), TZ = get_time_zone(), Tab = ets:new(ecron_timer, [ordered_set, private, {keypos, #timer.key}]), MaxTimeout = application:get_env(?Ecron, adjusting_time_second, 7 * 24 * 3600) * 1000, - init(Type, TZ, MaxTimeout, Tab). + init(Type, JobTab, Jobs, TZ, MaxTimeout, Tab). handle_call({add, Job, Options}, _From, State) -> #state{time_zone = TZ, timer_tab = Tab, job_tab = JobTab} = State, @@ -93,11 +97,12 @@ handle_call(statistic, _From, State = #state{timer_tab = Timer}) -> {reply, Res, State, next_timeout(State)}; handle_call({next_schedule_time, Name}, _From, State = #state{timer_tab = Timer}) -> - {reply, get_next_schedule_time(Timer, Name), State, next_timeout(State)}; + Reply = get_next_schedule_time(Timer, Name), + {reply, Reply, State, next_timeout(State)}; -handle_call(clear, _From, State = #state{timer_tab = Timer}) -> +handle_call(clear, _From, State = #state{timer_tab = Timer, job_tab = JobTab}) -> ets:delete_all_objects(Timer), - ets:delete_all_objects(?Job), + ets:delete_all_objects(JobTab), {reply, ok, State, next_timeout(State)}; handle_call(_Unknown, _From, State) -> @@ -106,8 +111,8 @@ handle_call(_Unknown, _From, State) -> handle_info(timeout, State) -> {noreply, State, tick(State)}; -handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab}) -> - pid_delete(Pid, TimerTab), +handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> + pid_delete(Pid, TimerTab, JobTab), {noreply, State, next_timeout(State)}; handle_info(_Unknown, State) -> @@ -120,22 +125,16 @@ handle_cast(_Unknown, State) -> %%% Internal functions %%%=================================================================== -init(local, TZ, MaxTimeout, Tab) -> - case parse_crontab(local_jobs(), []) of - {ok, Jobs} -> - [begin ets:insert_new(?Job, Job) end || Job <- Jobs], - [begin add_job(?Job, Tab, Job, TZ, Opts, true) - end || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(?Job)], - State = #state{max_timeout = MaxTimeout, time_zone = TZ, timer_tab = Tab, job_tab = ?Job}, - {ok, State, next_timeout(State)}; - Reason -> Reason - end; -init(global, TZ, MaxTimeout, Tab) -> - {ok, Jobs} = parse_crontab(global_jobs(), []), - ?GlobalJob = ets:new(?GlobalJob, [named_table, set, public, {keypos, 2}]), - [begin add_job(?GlobalJob, Tab, Job, TZ, Opts, true) +init(local, JobTab, Jobs, TZ, MaxTimeout, Tab) -> + [begin ets:insert_new(JobTab, Job) end || Job <- Jobs], + [begin add_job(JobTab, Tab, Job, TZ, Opts, true) + end || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(JobTab)], + State = #state{max_timeout = MaxTimeout, time_zone = TZ, timer_tab = Tab, job_tab = JobTab}, + {ok, State, next_timeout(State)}; +init(global, JobTab, Jobs, TZ, MaxTimeout, Tab) -> + [begin add_job(JobTab, Tab, Job, TZ, Opts, true) end || #job{job = Job, opts = Opts, status = activate} <- Jobs], - State = #state{max_timeout = MaxTimeout, time_zone = TZ, timer_tab = Tab, job_tab = ?GlobalJob}, + State = #state{max_timeout = MaxTimeout, time_zone = TZ, timer_tab = Tab, job_tab = JobTab}, {ok, State, next_timeout(State)}. parse_crontab([], Acc) -> {ok, Acc}; @@ -500,8 +499,6 @@ get_next_schedule_time(Timer, Name) -> end. get_time_zone() -> application:get_env(?Ecron, time_zone, local). -local_jobs() -> application:get_env(?Ecron, local_jobs, []). -global_jobs() -> application:get_env(?Ecron, global_jobs, []). maybe_spawn_worker(true, _, Name, {erlang, send, Args}, JobTab) -> {1, spawn_mfa(JobTab, Name, {erlang, send, Args})}; @@ -517,11 +514,11 @@ maybe_spawn_worker(true, Pid, Name, MFA, JobTab) when is_pid(Pid) -> end; maybe_spawn_worker(false, Singleton, _Name, _MFA, _JobTab) -> {0, Singleton}. -pid_delete(Pid, TimerTab) -> +pid_delete(Pid, TimerTab, JobTab) -> TimerMatch = [{#timer{link = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Pid}}]}], JobMatch = [{#job{link = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Pid}}]}], ets:select_delete(TimerTab, TimerMatch), - ets:select_delete(?Job, JobMatch). + ets:select_delete(JobTab, JobMatch). valid_opts(Opts) -> Singleton = proplists:get_value(singleton, Opts, true), diff --git a/test/prop_ecron_server.erl b/test/prop_ecron_server.erl index 0502281..2ada6ef 100644 --- a/test/prop_ecron_server.erl +++ b/test/prop_ecron_server.erl @@ -30,7 +30,7 @@ prop_server() -> application:ensure_all_started(ecron), ecron_tick:clear(), {History, State, Result} = run_commands(?MODULE, Cmds), - ets:delete_all_objects(?Job), + ets:delete_all_objects(?LocalJob), ?WHENFAIL(io:format("History: ~p\nState: ~p\nResult: ~p\n", [History, State, Result]), aggregate(command_names(Cmds), Result =:= ok)) diff --git a/test/prop_ecron_status.erl b/test/prop_ecron_status.erl index 5738029..fdea8e0 100644 --- a/test/prop_ecron_status.erl +++ b/test/prop_ecron_status.erl @@ -124,11 +124,11 @@ prop_unknown() -> ?FORALL(Message, term(), begin application:ensure_all_started(ecron), - Pid = erlang:whereis(?Ecron), + Pid = erlang:whereis(?LocalJob), CallRes = (catch gen_server:call(Pid, Message, 100)), gen_server:cast(Pid, Message), erlang:send(Pid, Message), - NewPid = erlang:whereis(?Ecron), + NewPid = erlang:whereis(?LocalJob), Pid =:= NewPid andalso {'EXIT', {timeout, {gen_server, call, [Pid, Message, 100]}}} =:= CallRes end). @@ -146,10 +146,10 @@ prop_restart_server() -> application:ensure_all_started(ecron), {ok, Name} = ecron:add(Name, "@yearly", {io, format, ["Yearly~n"]}), Res1 = ecron:statistic(Name), - Pid = erlang:whereis(?Ecron), + Pid = erlang:whereis(?LocalJob), erlang:exit(Pid, kill), timer:sleep(200), - NewPid = erlang:whereis(?Ecron), + NewPid = erlang:whereis(?LocalJob), Res2 = ecron:statistic(Name), ok = ecron:delete(Name), error_logger:tty(true), From 1d482a040b5c08de6b358955b27d353aab21c6c2 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 24 Nov 2020 14:45:03 +0800 Subject: [PATCH 02/11] rewrite ecron_tick behaviour to support multi job process --- include/ecron.hrl | 5 +- rebar.lock | 6 +- src/ecron.erl | 748 +++++++++++++++++++-------- src/ecron_app.erl | 1 - src/ecron_monitor.erl | 15 +- src/ecron_spec.erl | 286 ++++++++++ src/ecron_sup.erl | 35 +- src/ecron_tick.erl | 568 -------------------- test/prop_ecron.erl | 12 +- test/prop_ecron_app_config.erl | 2 +- test/prop_ecron_global_SUITE.erl | 48 +- test/prop_ecron_parse.erl | 32 +- test/prop_ecron_predict_datetime.erl | 10 +- test/prop_ecron_server.erl | 14 +- test/prop_ecron_status.erl | 6 +- 15 files changed, 897 insertions(+), 891 deletions(-) create mode 100644 src/ecron_spec.erl delete mode 100644 src/ecron_tick.erl diff --git a/include/ecron.hrl b/include/ecron.hrl index 02cffb1..f9e01d8 100644 --- a/include/ecron.hrl +++ b/include/ecron.hrl @@ -10,4 +10,7 @@ -define(Deactivate, [ecron, deactivate]). -define(Delete, [ecron, delete]). -define(GlobalUp, [ecron, global, up]). --define(GlobalDown, [ecron, global, down]). \ No newline at end of file +-define(GlobalDown, [ecron, global, down]). + +-record(job, {name, status = activate, job, opts = [], ok = 0, failed = 0, + link = undefined, result = [], run_microsecond = []}). \ No newline at end of file diff --git a/rebar.lock b/rebar.lock index 8c7563d..fa1dff2 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,6 +1,8 @@ -{"1.1.0", +{"1.2.0", [{<<"telemetry">>,{pkg,<<"telemetry">>,<<"0.4.0">>},0}]}. [ {pkg_hash,[ - {<<"telemetry">>, <<"8339BEE3FA8B91CB84D14C2935F8ECF399CCD87301AD6DA6B71C09553834B2AB">>}]} + {<<"telemetry">>, <<"8339BEE3FA8B91CB84D14C2935F8ECF399CCD87301AD6DA6B71C09553834B2AB">>}]}, +{pkg_hash_ext,[ + {<<"telemetry">>, <<"E9E3CACFD37C1531C0CA70CA7C0C30CE2DBB02998A4F7719DE180FE63F8D41E4">>}]} ]. diff --git a/src/ecron.erl b/src/ecron.erl index d07703a..a6ed3ad 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -1,6 +1,8 @@ -module(ecron). -include("ecron.hrl"). +-export([predict_datetime/2]). +%% API Function -export([add/3, add/6]). -export([add_with_datetime/4, add_with_datetime/5]). -export([add_with_count/3, add_with_count/4]). @@ -10,9 +12,24 @@ -export([deactivate/1, activate/1]). -export([statistic/0, statistic/1]). -export([reload/0]). --export([parse_spec/2, parse_spec/1]). --export([valid_datetime/2]). +-export([parse_spec/2]). +%% CallBack Function +-export([start_link/2, handle_call/3, handle_info/2, init/1, handle_cast/2]). +-export([spawn_mfa/3, clear/0]). + +-record(state, {time_zone, max_timeout, timer_tab, job_tab}). +-record(timer, {key, name, cur_count = 0, singleton, type, spec, mfa, link, + start_sec = unlimited, end_sec = unlimited, max_count = unlimited}). + +-define(MAX_SIZE, 16). +-define(SECONDS_FROM_0_TO_1970, 719528 * 86400). +-define(day_of_week(Y, M, D), (case calendar:day_of_the_week(Y, M, D) of 7 -> 0; D1 -> D1 end)). +-define(MatchNameSpec(Name), [{#timer{name = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Name}}]}]). + +%%%=================================================================== +%%% API +%%%=================================================================== -type name() :: term(). -type crontab_spec() :: crontab() | string() | binary() | 1..4294967. @@ -94,16 +111,18 @@ add_with_datetime(JobName, Spec, MFA, Start, End) -> %% -spec add(name(), crontab_spec(), mfargs(), start_datetime(), end_datetime(), options()) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. -add(JobName, Spec, MFA, Start, End, Option) -> - case valid_datetime(Start, End) of +add(JobName, Spec, MFA, Start, End, Opts) -> + case ecron_spec:is_start_end_ok(Start, End) of true -> - case parse_spec(Spec) of + case ecron_spec:parse_spec(Spec) of {ok, Type, Crontab} -> - ecron_tick:add(#{ + Job = #{ type => Type, name => JobName, crontab => Crontab, mfa => MFA, start_time => Start, end_time => End - }, Option); + }, + ValidOpts = ecron_spec:parse_valid_opts(Opts), + gen_server:call(?LocalJob, {add, Job, ValidOpts}, infinity); ErrParse -> ErrParse end; false -> {error, invalid_time, {Start, End}} @@ -166,37 +185,54 @@ send_interval(JobName, Spec, Pid, Message, Start, End, Option) -> %% @doc %% Delete an exist job, if the job is nonexistent, nothing happened. -spec delete(name()) -> ok. -delete(JobName) -> ecron_tick:delete(JobName). +delete(JobName) -> gen_server:call(?LocalJob, {delete, JobName}, infinity). %% @doc %% deactivate an exist job, if the job is nonexistent, return `{error, not_found}'. %% just freeze the job, use @see activate/1 to unfreeze job. -spec deactivate(name()) -> ok | {error, not_found}. -deactivate(JobName) -> ecron_tick:deactivate(JobName). +deactivate(JobName) -> gen_server:call(?LocalJob, {deactivate, JobName}, infinity). %% @doc %% activate an exist job, if the job is nonexistent, return `{error, not_found}'. %% if the job is already activate, nothing happened. %% the same effect as reinstall the job from now on. -spec activate(name()) -> ok | {error, not_found}. -activate(JobName) -> ecron_tick:activate(JobName). +activate(JobName) -> gen_server:call(?LocalJob, {activate, JobName}, infinity). %% @doc %% Statistic from an exist job. %% if the job is nonexistent, return `{error, not_found}'. -spec statistic(name()) -> {ok, statistic()} | {error, not_found}. -statistic(JobName) -> ecron_tick:statistic(JobName). +statistic(JobName) -> + case ets:lookup(?LocalJob, JobName) of + [Job] -> {ok, job_to_statistic(Job)}; + [] -> + try + gen_server:call({global, ?GlobalJob}, {statistic, JobName}) + catch _:_ -> + {error, not_found} + end + end. %% @doc %% Statistic for all jobs. -spec statistic() -> [statistic()]. -statistic() -> ecron_tick:statistic(). +statistic() -> + Local = ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], ?LocalJob), + Global = + try + gen_server:call({global, ?GlobalJob}, statistic) + catch _:_ -> + [] + end, + Local ++ Global. %% @doc %% reload task manually, such as you should reload manually when the system time has alter a lot. -spec reload() -> ok. reload() -> - ecron_tick:reload(), + gen_server:cast(?LocalJob, reload), gen_server:cast({global, ecron_global}, reload). %% @doc @@ -205,252 +241,506 @@ reload() -> {ok, #{type => cron | every, crontab => crontab_spec(), next => [calendar:rfc3339_string()]}} | {error, atom(), term()}. parse_spec(Spec, Num) when is_integer(Num) andalso Num > 0 -> - parse_spec_2(parse_spec(Spec), Num). - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== + parse_spec_2(ecron_spec:parse_spec(Spec), Num). parse_spec_2({ok, Type, JobSpec}, Num) -> Job = #{type => Type, crontab => JobSpec}, - Next = ecron_tick:predict_datetime(Job, Num), + Next = predict_datetime(Job, Num), {ok, Job#{next => Next}}; parse_spec_2({error, _Field, _Value} = Error, _Num) -> Error. -%% @private -valid_datetime(Start, End) -> - case valid_datetime(Start) andalso valid_datetime(End) of - true when Start =/= unlimited andalso End =/= unlimited -> - EndSec = calendar:datetime_to_gregorian_seconds(End), - StartSec = calendar:datetime_to_gregorian_seconds(Start), - EndSec > StartSec; - Res -> Res +get_next_schedule_time(Name) -> gen_server:call(?LocalJob, {next_schedule_time, Name}, infinity). +clear() -> gen_server:call(?LocalJob, clear, infinity). + +predict_datetime(Job, Num) -> + TZ = get_time_zone(), + Now = current_millisecond(), + predict_datetime(activate, Job, unlimited, unlimited, Num, TZ, Now). + +%%%=================================================================== +%%% CallBack +%%%=================================================================== +start_link({_, JobTab} = Name, JobSpec) -> + case ecron_spec:parse_crontab(JobSpec, []) of + {ok, Jobs} -> + new_job_tab(JobTab), + gen_server:start_link(Name, ?MODULE, [JobTab, Jobs], []); + {error, Reason} -> {error, Reason} end. -valid_datetime(unlimited) -> true; -valid_datetime({Date, {H, M, S}}) -> - (is_integer(H) andalso H >= 0 andalso H =< 23) andalso - (is_integer(M) andalso M >= 0 andalso M =< 59) andalso - (is_integer(S) andalso S >= 0 andalso H =< 59) andalso - calendar:valid_date(Date); -valid_datetime(_ErrFormat) -> false. - -%% @private -parse_spec("@yearly") -> parse_spec("0 0 1 1 *"); % Run once a year, midnight, Jan. 1st -parse_spec("@annually") -> parse_spec("0 0 1 1 *"); % Same as @yearly -parse_spec("@monthly") -> parse_spec("0 0 1 * *"); % Run once a month, midnight, first of month -parse_spec("@weekly") -> parse_spec("0 0 * * 0"); % Run once a week, midnight between Sat/Sun -parse_spec("@midnight") -> parse_spec("0 0 * * *"); % Run once a day, midnight -parse_spec("@daily") -> parse_spec("0 0 * * *"); % Same as @midnight -parse_spec("@hourly") -> parse_spec("0 * * * *"); % Run once an hour, beginning of hour -parse_spec("@minutely") -> parse_spec("* * * * *"); -parse_spec(Bin) when is_binary(Bin) -> parse_spec(binary_to_list(Bin)); -parse_spec(List) when is_list(List) -> - case string:tokens(string:lowercase(List), " ") of - [_S, _M, _H, _DOM, _Mo, _DOW] = Cron -> parse_cron_spec(Cron); - [_M, _H, _DOM, _Mo, _DOW] = Cron -> parse_cron_spec(["0" | Cron]); - ["@every", Sec] -> parse_every_spec(Sec); - _ -> {error, invalid_spec, List} - end; -parse_spec(Spec) when is_map(Spec) -> - {Months, NewSpec} = take(month, Spec), - case unzip(Months, 1, 12, []) of - {ok, EMonths} -> - List = [{second, 0, 59}, {minute, 0, 59}, {hour, 0, 23}, - {day_of_month, 1, get_max_day_of_months(EMonths)}, - {day_of_week, 0, 6}], - format_map_spec(List, NewSpec, #{month => zip(EMonths)}); - error -> {error, month, Months} - end; -parse_spec(Second) when is_integer(Second) andalso Second =< ?MAX_TIMEOUT -> - {ok, every, Second}; -parse_spec(Spec) -> {error, invalid_spec, Spec}. - -parse_cron_spec([Second, Minute, Hour, DayOfMonth, Month, DayOfWeek]) -> - case parse_field(Month, 1, 12) of - {ok, Months} -> - Fields = [ - {second, Second, 0, 59}, - {minute, Minute, 0, 59}, - {hour, Hour, 0, 23}, - {day_of_month, DayOfMonth, 1, get_max_day_of_months(Months)}, - {day_of_week, DayOfWeek, 0, 6}], - parse_fields(Fields, #{month => Months}); - error -> {error, month, Month} +init([JobTab, Jobs]) -> + erlang:process_flag(trap_exit, true), + TimerTab = ets:new(ecron_timer, [ordered_set, private, {keypos, #timer.key}]), + TimeZone = get_time_zone(), + MaxTimeout = application:get_env(?Ecron, adjusting_time_second, 7 * 24 * 3600) * 1000, + + [begin ets:insert_new(JobTab, Job) end || Job <- Jobs], + [begin add_job(JobTab, TimerTab, Job, TimeZone, Opts, true) + end || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(JobTab)], + + State = #state{timer_tab = TimerTab, job_tab = JobTab, max_timeout = MaxTimeout, time_zone = TimeZone}, + {ok, State, next_timeout(State)}. + +handle_call({add, Job, Opts}, _From, State) -> + #state{timer_tab = TimerTab, job_tab = JobTab, time_zone = TimeZone} = State, + Reply = add_job(JobTab, TimerTab, Job, TimeZone, Opts, false), + {reply, Reply, State, tick(State)}; + +handle_call({delete, Name}, _From, State) -> + #state{timer_tab = TimerTab, job_tab = JobTab} = State, + delete_job(JobTab, TimerTab, Name), + {reply, ok, State, next_timeout(State)}; + +handle_call({activate, Name}, _From, State) -> + #state{job_tab = JobTab, time_zone = TimeZone, timer_tab = TimerTab} = State, + Reply = activate_job(Name, JobTab, TimerTab, TimeZone), + {reply, Reply, State, tick(State)}; + +handle_call({deactivate, Name}, _From, State) -> + #state{timer_tab = TimerTab, job_tab = JobTab} = State, + Reply = deactivate_job(Name, JobTab, TimerTab), + {reply, Reply, State, next_timeout(State)}; + +handle_call({statistic, Name}, _From, State) -> + Reply = job_to_statistic(Name, State), + {reply, Reply, State, next_timeout(State)}; + +handle_call(statistic, _From, State = #state{timer_tab = TimerTab}) -> + Reply = + ets:foldl(fun(#timer{name = Name}, Acc) -> + {ok, Item} = job_to_statistic(Name, State), + [Item | Acc] + end, [], TimerTab), + {reply, Reply, State, next_timeout(State)}; + +handle_call({next_schedule_time, Name}, _From, State = #state{timer_tab = TimerTab}) -> + Reply = get_next_schedule_time(TimerTab, Name), + {reply, Reply, State, next_timeout(State)}; + +handle_call(clear, _From, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> + ets:delete_all_objects(TimerTab), + ets:delete_all_objects(JobTab), + {reply, ok, State, next_timeout(State)}; + +handle_call(_Unknown, _From, State) -> + {noreply, State, next_timeout(State)}. + +handle_info(timeout, State) -> + {noreply, State, tick(State)}; + +handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> + delete_pid(Pid, TimerTab, JobTab), + {noreply, State, next_timeout(State)}; + +handle_info(_Unknown, State) -> + {noreply, State, next_timeout(State)}. + +handle_cast(_Unknown, State) -> + {noreply, State, next_timeout(State)}. + +%%%=================================================================== +%%% First Internal Functions +%%%=================================================================== +new_job_tab(JobTab) -> + case ets:info(JobTab) of + undefined -> ets:new(JobTab, [named_table, set, public, {keypos, #job.name}]); + _ -> ok end. -parse_fields([], Acc) -> {ok, cron, Acc}; -parse_fields([{Key, Spec, Min, Max} | Rest], Acc) -> - case parse_field(Spec, Min, Max) of - {ok, V} -> parse_fields(Rest, Acc#{Key => V}); - error -> {error, Key, Spec} +add_job(JobTab, TimerTab, Job, TimeZone, Opts, ForceUpdate) -> + #{name := Name, mfa := MFA} = Job, + PidOrUndef = link_send_pid(MFA), + JobRec = #job{status = activate, name = Name, job = Job, opts = Opts, link = PidOrUndef}, + IsNew = ets:insert_new(JobTab, JobRec), + case IsNew orelse ForceUpdate of + true -> + Singleton = proplists:get_value(singleton, Opts), + MaxCount = proplists:get_value(max_count, Opts), + InitTimer = #timer{singleton = Singleton, max_count = MaxCount, + name = Name, mfa = MFA, link = PidOrUndef}, + Now = current_millisecond(), + telemetry:execute(?Activate, #{action_ms => Now}, #{name => Name, mfa => MFA}), + update_timer(Now, InitTimer, Job, TimerTab, JobTab, TimeZone); + false -> {error, already_exist} end. -parse_field("*", _Min, _Max) -> {ok, '*'}; -parse_field(Value, MinLimit, MaxLimit) -> - parse_field(string:tokens(Value, ","), MinLimit, MaxLimit, []). - -parse_field([], _MinLimit, _MaxLimit, Acc) -> {ok, zip(lists:usort(Acc))}; -parse_field([Field | Fields], MinL, MaxL, Acc) -> - case string:tokens(Field, "-") of - [Field] -> - case string:tokens(Field, "/") of - [_] -> % Integer - Int = field_to_int(Field), - case Int >= MinL andalso Int =< MaxL of - true -> parse_field(Fields, MinL, MaxL, [Int | Acc]); - false -> error - end; - ["*", StepStr] -> % */Step -> MinLimit~MaxLimit/Step - case field_to_int(StepStr) of - Step when Step > 0 -> - NewAcc = lists:seq(MinL, MaxL, Step) ++ Acc, - parse_field(Fields, MinL, MaxL, NewAcc); - _ -> error - end; - [MinStr, StepStr] -> % Min/Step -> Min~MaxLimit/Step - Min = field_to_int(MinStr), - Step = field_to_int(StepStr), - case Min >= MinL andalso Min =< MaxL andalso Step > 0 of - true -> - NewAcc = lists:seq(Min, MaxL, Step) ++ Acc, - parse_field(Fields, MinL, MaxL, NewAcc); - false -> error - end; - _ -> error - end; - [MinStr, MaxStepStr] -> - case field_to_int(MinStr) of - Min when Min >= MinL andalso Min =< MaxL -> % Min-Max/Step -> Min~Max/Step - {Max, Step} = - case string:tokens(MaxStepStr, "/") of - [_] -> {field_to_int(MaxStepStr), 1}; - [MaxStr, StepStr] -> {field_to_int(MaxStr), field_to_int(StepStr)}; - _ -> {-1, -1} %% error - end, - case Max >= MinL andalso Max >= Min andalso Step > 0 of +activate_job(Name, JobTab, TimerTab, TimeZone) -> + case ets:lookup(JobTab, Name) of + [] -> {error, not_found}; + [#job{job = Job, opts = Opts}] -> + delete_job(JobTab, TimerTab, Name), + case add_job(JobTab, TimerTab, Job, TimeZone, Opts, false) of + {ok, Name} -> ok; + Err -> Err + end + end. + +deactivate_job(Name, JobTab, TimerTab) -> + ets:select_delete(TimerTab, ?MatchNameSpec(Name)), + case ets:update_element(JobTab, Name, {#job.status, deactivate}) of + true -> + telemetry:execute(?Deactivate, #{action_ms => current_millisecond()}, #{name => Name}), + ok; + false -> {error, not_found} + end. + +delete_job(JobTab, TimerTab, Name) -> + ets:select_delete(TimerTab, ?MatchNameSpec(Name)), + case ets:lookup(JobTab, Name) of + [] -> ok; + [#job{link = Link}] -> + telemetry:execute(?Delete, #{action_ms => current_millisecond()}, #{name => Name}), + unlink_pid(Link), + ets:delete(JobTab, Name) + end. + +%%%=================================================================== +%%% Second Internal Functions +%%%=================================================================== +update_timer(Now, InitTimer, Job, TimeTab, JobTab, TimeZone) -> + #{name := Name, crontab := Spec, type := Type, start_time := StartTime, end_time := EndTime} = Job, + Start = datetime_to_millisecond(TimeZone, StartTime), + End = datetime_to_millisecond(TimeZone, EndTime), + case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of + {ok, NextSec} -> + Timer = InitTimer#timer{key = {NextSec, Name}, type = Type, + spec = Spec, start_sec = Start, end_sec = End}, + ets:insert(TimeTab, Timer), + {ok, Name}; + {error, already_ended} = Err -> + delete_job(JobTab, TimeTab, Name), + Err + end. + +next_schedule_millisecond(every, Sec, _TimeZone, Now, Start, End) -> + Next = Now + Sec * 1000, + case in_range(Next, Start, End) of + {error, deactivate} -> {ok, Start}; + {error, already_ended} -> {error, already_ended}; + ok -> {ok, Next} + end; +next_schedule_millisecond(cron, Spec, TimeZone, Now, Start, End) -> + ForwardDateTime = millisecond_to_datetime(TimeZone, Now + 1000), + DefaultMin = #{second => 0, minute => 0, hour => 0, + day_of_month => 1, month => 1, day_of_week => 0}, + Min = spec_min(maps:to_list(Spec), DefaultMin), + NextDateTime = next_schedule_datetime(Spec, Min, ForwardDateTime), + Next = datetime_to_millisecond(TimeZone, NextDateTime), + case in_range(Next, Start, End) of + {error, deactivate} -> next_schedule_millisecond(cron, Spec, TimeZone, Start - 1000, Start, End); + {error, already_ended} -> {error, already_ended}; + ok -> {ok, Next} + end. + +next_schedule_datetime(DateSpec, Min, DateTime) -> + #{ + second := SecondSpec, minute := MinuteSpec, hour := HourSpec, + day_of_month := DayOfMonthSpec, month := MonthSpec, + day_of_week := DayOfWeekSpec} = DateSpec, + {{Year, Month, Day}, {Hour, Minute, Second}} = DateTime, + case valid_datetime(MonthSpec, Month) of + false -> forward_month(DateTime, Min, DateSpec); + true -> + case valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) of + false -> + LastDay = calendar:last_day_of_the_month(Year, Month), + forward_day(DateTime, Min, LastDay, DateSpec); + true -> + case valid_datetime(HourSpec, Hour) of + false -> forward_hour(DateTime, Min, DateSpec); true -> - New = lists:seq(Min, Max, Step), - case lists:max(New) =< MaxL of - true -> parse_field(Fields, MinL, MaxL, New ++ Acc); - false -> error - end; - false -> error - end; - _ -> error - end; - _ -> error + case valid_datetime(MinuteSpec, Minute) of + false -> forward_minute(DateTime, Min, DateSpec); + true -> + case valid_datetime(SecondSpec, Second) of + false -> forward_second(DateTime, Min, DateSpec); + true -> DateTime + end + end + end + end end. -zip('*') -> '*'; -zip([T | Rest]) -> zip(Rest, T + 1, [T], []). - -zip([], _, [Single], Acc) -> lists:reverse([Single | Acc]); -zip([], _, [One, Two], Acc) -> lists:reverse([One, Two | Acc]); -zip([], _, Buffer, Acc) -> lists:reverse([{lists:min(Buffer), lists:max(Buffer)} | Acc]); -zip([L | Rest], L, Buffer, Acc) -> zip(Rest, L + 1, [L | Buffer], Acc); -zip([F | Rest], _Last, [Single], Acc) -> zip(Rest, F + 1, [F], [Single | Acc]); -zip([F | Rest], _Last, [One, Two], Acc) -> zip(Rest, F + 1, [F], [One, Two | Acc]); -zip([F | Rest], _Last, Buffer, Acc) -> - zip(Rest, F + 1, [F], [{lists:min(Buffer), lists:max(Buffer)} | Acc]). - -unzip('*', _MinLimit, _MaxLimit, _Acc) -> {ok, '*'}; -unzip([], _MinLimit, _MaxLimit, Acc) -> {ok, lists:usort(Acc)}; -unzip([{Min, Max} | List], MinL, MaxL, Acc) -> - NewMin = field_to_int(Min), - NewMax = field_to_int(Max), - case NewMax >= NewMin andalso NewMax =< MaxL andalso NewMin >= MinL of - true -> unzip(List, MinL, MaxL, lists:seq(NewMin, NewMax) ++ Acc); - false -> error +tick(State = #state{timer_tab = TimerTab}) -> + tick_tick(ets:first(TimerTab), current_millisecond(), State). + +current_millisecond() -> erlang:system_time(millisecond). + +tick_tick('$end_of_table', _Cur, _State) -> infinity; +tick_tick({Due, _Name}, Cur, #state{max_timeout = MaxTimeout}) when Due > Cur -> + min(Due - Cur, MaxTimeout); +tick_tick(Key = {Due, Name}, Cur, State) -> + #state{time_zone = TZ, timer_tab = TimerTab, job_tab = JobTab} = State, + [Cron] = ets:lookup(TimerTab, Key), + #timer{singleton = Singleton, mfa = MFA, max_count = MaxCount, cur_count = CurCount} = Cron, + ets:delete(TimerTab, Key), + {Incr, CurPid} = maybe_spawn_worker(Cur - Due < 1000, Singleton, Name, MFA, JobTab), + update_next_schedule(CurCount + Incr, MaxCount, Cron, Cur, Name, TZ, CurPid, TimerTab, JobTab), + tick(State). + +maybe_spawn_worker(true, _, Name, {erlang, send, Args}, JobTab) -> + {1, spawn_mfa(JobTab, Name, {erlang, send, Args})}; +maybe_spawn_worker(true, true, Name, MFA, JobTab) -> + {1, spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA])}; +maybe_spawn_worker(true, false, Name, MFA, JobTab) -> + spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA]), + {1, false}; +maybe_spawn_worker(true, Pid, Name, MFA, JobTab) when is_pid(Pid) -> + case is_process_alive(Pid) of + true -> {0, Pid}; + false -> {1, spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA])} end; -unzip([Int | List], MinL, MaxL, Acc) -> - case field_to_int(Int) of - V when V >= MinL andalso V =< MaxL -> unzip(List, MinL, MaxL, [V | Acc]); - _ -> error +maybe_spawn_worker(false, Singleton, _Name, _MFA, _JobTab) -> {0, Singleton}. + +update_next_schedule(Max, Max, _Cron, _Cur, Name, _TZ, _CurPid, Tab, JobTab) -> delete_job(JobTab, Tab, Name); +update_next_schedule(Count, _Max, Cron, Cur, Name, TZ, CurPid, Tab, JobTab) -> + #timer{type = Type, start_sec = Start, end_sec = End, spec = Spec} = Cron, + case next_schedule_millisecond(Type, Spec, TZ, Cur, Start, End) of + {ok, Next} -> + NextTimer = Cron#timer{key = {Next, Name}, singleton = CurPid, cur_count = Count}, + ets:insert(Tab, NextTimer); + {error, already_ended} -> + delete_job(JobTab, Tab, Name) end. +spawn_mfa(JobTab, Name, MFA) -> + Start = erlang:monotonic_time(), + {Event, OkInc, FailedInc, NewRes} = + try + case MFA of + {erlang, send, [Pid, Message]} -> + erlang:send(Pid, Message), + {?Success, 1, 0, Message}; + {M, F, A} -> {?Success, 1, 0, apply(M, F, A)}; + {F, A} -> {?Success, 1, 0, apply(F, A)} + end + catch + Error:Reason:Stacktrace -> + {?Failure, 0, 1, {Error, Reason, Stacktrace}} + end, + End = erlang:monotonic_time(), + Cost = erlang:convert_time_unit(End - Start, native, microsecond), + telemetry:execute(Event, #{run_microsecond => Cost, run_result => NewRes}, #{name => Name, mfa => MFA}), + case ets:lookup(JobTab, Name) of + [] -> ok; + [Job] -> + #job{ok = Ok, failed = Failed, run_microsecond = RunMs, result = Results} = Job, + Elements = [{#job.ok, Ok + OkInc}, {#job.failed, Failed + FailedInc}, + {#job.run_microsecond, lists:sublist([Cost | RunMs], ?MAX_SIZE)}, + {#job.result, lists:sublist([NewRes | Results], ?MAX_SIZE)}], + ets:update_element(JobTab, Name, Elements) + end. -parse_every_spec(SecSpec) -> - LowerSecSpec = string:lowercase(SecSpec), - List = [{"d", 24 * 3600}, {"h", 3600}, {"m", 60}, {"s", 1}], - case parse_every(List, LowerSecSpec, 0) of - {ok, Sec} when Sec > 0 andalso Sec =< ?MAX_TIMEOUT -> {ok, every, Sec}; - {ok, Sec} -> {error, second, Sec}; - error -> {error, second, SecSpec} +forward_second(DateTime, Min, Spec) -> + {{Year, Month, Day}, {Hour, Minute, Second}} = DateTime, + NewSecond = nearest(second, Second, 59, Spec), + case Second >= NewSecond of + true -> forward_minute(DateTime, Min, Spec); + false -> {{Year, Month, Day}, {Hour, Minute, NewSecond}} end. -parse_every(_, "", Sum) -> {ok, Sum}; -parse_every([], _, _Sum) -> error; -parse_every([{Sep, Index} | Rest], Spec, Sum) -> - case parse_every(Spec, Sep) of - {Val, NewSpec} -> parse_every(Rest, NewSpec, Val * Index + Sum); - error -> error +forward_minute(DateTime, Min, Spec) -> + {{Year, Month, Day}, {Hour, Minute, _Second}} = DateTime, + NewMinute = nearest(minute, Minute, 59, Spec), + case Minute >= NewMinute of + true -> forward_hour(DateTime, Min, Spec); + false -> + #{second := SecondM} = Min, + {{Year, Month, Day}, {Hour, NewMinute, SecondM}} end. -parse_every(Spec, Seps) -> - case string:tokens(Spec, Seps) of - [Spec] -> {0, Spec}; - [Str, S] -> - case field_to_int(Str) of - Value when Value >= 0 -> {Value, S}; - _ -> error - end; - [Str] -> - case field_to_int(Str) of - Value when Value >= 0 -> {Value, ""}; - _ -> error - end; - _ -> error +forward_hour(DateTime, Min, Spec) -> + {{Year, Month, Day}, {Hour, _Minute, _Second}} = DateTime, + NewHour = nearest(hour, Hour, 23, Spec), + case Hour >= NewHour of + true -> + LastDay = calendar:last_day_of_the_month(Year, Month), + forward_day(DateTime, Min, LastDay, Spec); + false -> + #{minute := MinuteM, second := SecondM} = Min, + {{Year, Month, Day}, {NewHour, MinuteM, SecondM}} end. -get_max_day_of_months('*') -> 31; -get_max_day_of_months(List) -> max_day_of_months(List, 29). - -max_day_of_months([], Max) -> Max; -max_day_of_months(_, 31) -> 31; -max_day_of_months([{_Min, _Max} | _List], _OldMax) -> 31; %% because Max - Min >= 2 -max_day_of_months([Int | List], Max) -> - NewMax = erlang:max(Max, last_day_of_month(Int)), - max_day_of_months(List, NewMax). - -last_day_of_month(2) -> 29; -last_day_of_month(4) -> 30; -last_day_of_month(6) -> 30; -last_day_of_month(9) -> 30; -last_day_of_month(11) -> 30; -last_day_of_month(M) when is_integer(M), M > 0, M < 13 -> 31. - --define(Alphabet, #{ - "sun" => 0, "mon" => 1, "tue" => 2, "wed" => 3, "thu" => 4, "fir" => 5, "sat" => 6, - "jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6, - "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12}). - -field_to_int(Int) when is_integer(Int) -> Int; -field_to_int(List) when is_list(List) -> - case maps:find(List, ?Alphabet) of - error -> - case string:list_to_integer(List) of - {Int, []} -> Int; - _ -> -1 %% error - end; - {ok, Int} -> Int +forward_day(DateTime, Min, LastDay, Spec) -> + {{Year, Month, Day}, {_Hour, _Minute, _Second}} = DateTime, + case Day + 1 of + NewDay when NewDay > LastDay -> forward_month(DateTime, Min, Spec); + NewDay -> + #{hour := HourM, minute := MinuteM, second := SecondM} = Min, + NewDateTime = {{Year, Month, NewDay}, {HourM, MinuteM, SecondM}}, + #{day_of_week := DayOfWeekSpec, day_of_month := DayOfMonthSpec} = Spec, + case valid_day(Year, Month, NewDay, DayOfMonthSpec, DayOfWeekSpec) of + true -> NewDateTime; + false -> forward_day(NewDateTime, Min, LastDay, Spec) + end + end. + +forward_month(DateTime, Min, Spec) -> + {{Year, Month, _Day}, {_Hour, _Minute, _Second}} = DateTime, + NewMonth = nearest(month, Month, 12, Spec), + #{month := MonthM, hour := HourM, minute := MinuteM, second := SecondM} = Min, + NewDateTime = + {{NYear, NMonth, NDay}, {_NHour, _NMinute, _NSecond}} = + case Month >= NewMonth of + true -> {{Year + 1, MonthM, 1}, {HourM, MinuteM, SecondM}}; + false -> {{Year, NewMonth, 1}, {HourM, MinuteM, SecondM}} + end, + #{day_of_week := DayOfWeekSpec, day_of_month := DayOfMonthSpec} = Spec, + case valid_day(NYear, NMonth, NDay, DayOfMonthSpec, DayOfWeekSpec) of + false -> + LastDay = calendar:last_day_of_the_month(NYear, NMonth), + forward_day(NewDateTime, Min, LastDay, Spec); + true -> NewDateTime end. -format_map_spec([], Old, New) when Old =:= #{} -> {ok, cron, New}; -format_map_spec([], Old, _New) -> {error, maps:keys(Old), maps:values(Old)}; -format_map_spec([{Key, Min, Max} | List], Old, New) -> - {Value, Old1} = take(Key, Old), - case unzip(Value, Min, Max, []) of - {ok, EValue} -> format_map_spec(List, Old1, New#{Key => zip(EValue)}); - error -> {error, Key, Value} +datetime_to_millisecond(_, unlimited) -> unlimited; +datetime_to_millisecond(local, DateTime) -> + UtcTime = erlang:localtime_to_universaltime(DateTime), + datetime_to_millisecond(utc, UtcTime); +datetime_to_millisecond(utc, DateTime) -> + (calendar:datetime_to_gregorian_seconds(DateTime) - ?SECONDS_FROM_0_TO_1970) * 1000. + +millisecond_to_datetime(local, Ms) -> calendar:system_time_to_local_time(Ms, millisecond); +millisecond_to_datetime(utc, Ms) -> calendar:system_time_to_universal_time(Ms, millisecond). + +nearest(Type, Current, Max, Spec) -> + Values = maps:get(Type, Spec), + nearest_1(Values, Values, Max, Current + 1). + +nearest_1('*', '*', MaxLimit, Next) when Next > MaxLimit -> 1; +nearest_1('*', '*', _MaxLimit, Next) -> Next; +nearest_1([], [{Min, _} | _], _Max, _Next) -> Min; +nearest_1([], [Min | _], _Max, _Next) -> Min; +nearest_1([{Min, Max} | Rest], Spec, MaxLimit, Next) -> + if + Next > Max -> nearest_1(Rest, Spec, MaxLimit, Next); + Next =< Min -> Min; + true -> Next + end; +nearest_1([Expect | Rest], Spec, MaxLimit, Next) -> + case Next > Expect of + true -> nearest_1(Rest, Spec, MaxLimit, Next); + false -> Expect end. -take(Key, Spec) -> - case maps:take(Key, Spec) of - error when Key =:= second -> {[0], Spec}; - error -> {'*', Spec}; - Res -> Res +valid_datetime('*', _Value) -> true; +valid_datetime([], _Value) -> false; +valid_datetime([Value | _T], Value) -> true; +valid_datetime([{Lower, Upper} | _], Value) when Lower =< Value andalso Value =< Upper -> true; +valid_datetime([_ | T], Value) -> valid_datetime(T, Value). + +valid_day(_Year, _Month, _Day, '*', '*') -> true; +valid_day(_Year, _Month, Day, DayOfMonthSpec, '*') -> + valid_datetime(DayOfMonthSpec, Day); +valid_day(Year, Month, Day, '*', DayOfWeekSpec) -> + DayOfWeek = ?day_of_week(Year, Month, Day), + valid_datetime(DayOfWeekSpec, DayOfWeek); +valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) -> + case valid_datetime(DayOfMonthSpec, Day) of + false -> + DayOfWeek = ?day_of_week(Year, Month, Day), + valid_datetime(DayOfWeekSpec, DayOfWeek); + true -> true end. +spec_min([], Acc) -> Acc; +spec_min([{Key, Value} | Rest], Acc) -> + NewAcc = + case Value of + '*' -> Acc; + [{Min, _} | _] -> Acc#{Key => Min}; + [Min | _] -> Acc#{Key => Min} + end, + spec_min(Rest, NewAcc). + +next_timeout(#state{timer_tab = TimerTab, max_timeout = MaxTimeout}) -> + case ets:first(TimerTab) of + '$end_of_table' -> infinity; + {Due, _} -> min(max(Due - current_millisecond(), 0), MaxTimeout) + end. + +in_range(_Current, unlimited, unlimited) -> ok; +in_range(Current, unlimited, End) when Current > End -> {error, already_ended}; +in_range(_Current, unlimited, _End) -> ok; +in_range(Current, Start, unlimited) when Current < Start -> {error, deactivate}; +in_range(_Current, _Start, unlimited) -> ok; +in_range(Current, _Start, End) when Current > End -> {error, already_ended}; +in_range(Current, Start, _End) when Current < Start -> {error, deactivate}; +in_range(_Current, _Start, _End) -> ok. + +to_rfc3339(unlimited) -> unlimited; +to_rfc3339(Next) -> calendar:system_time_to_rfc3339(Next div 1000, [{unit, second}]). + +predict_datetime(deactivate, _, _, _, _, _, _) -> []; +predict_datetime(activate, #{type := every, crontab := Sec} = Job, Start, End, Num, TimeZone, NowT) -> + Now = case maps:find(name, Job) of error -> NowT; _ -> NowT - Sec * 1000 end, + predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []); +predict_datetime(activate, Job, Start, End, Num, TimeZone, Now) -> + predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []). + +predict_datetime_2(_Job, _TimeZone, _Now, _Start, _End, 0, Acc) -> lists:reverse(Acc); +predict_datetime_2(Job, TimeZone, Now, Start, End, Num, Acc) -> + #{type := Type, crontab := Spec} = Job, + case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of + {ok, Next} -> + NewAcc = [to_rfc3339(Next) | Acc], + predict_datetime_2(Job, TimeZone, Next, Start, End, Num - 1, NewAcc); + {error, already_ended} -> lists:reverse(Acc) + end. + +get_next_schedule_time(Timer, Name) -> + %% P = ets:fun2ms(fun(#timer{name = N, key = {Time, _}}) when N =:= Name -> Time end), + P = [{#timer{key = {'$1', '_'}, name = '$2', _ = '_'}, [{'=:=', '$2', {const, Name}}], ['$1']}], + case ets:select(Timer, P) of + [T] -> T; + [] -> current_millisecond() + end. + +get_time_zone() -> application:get_env(?Ecron, time_zone, local). + +delete_pid(Pid, TimerTab, JobTab) -> + TimerMatch = [{#timer{link = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Pid}}]}], + JobMatch = [{#job{link = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Pid}}]}], + ets:select_delete(TimerTab, TimerMatch), + ets:select_delete(JobTab, JobMatch). + +link_send_pid({erlang, send, [PidOrName, _Message]}) -> + Pid = get_pid(PidOrName), + is_pid(Pid) andalso (catch link(Pid)), + Pid; +link_send_pid(_MFA) -> undefined. + +unlink_pid(Pid) when is_pid(Pid) -> catch unlink(Pid); +unlink_pid(_) -> ok. + +get_pid(Pid) when is_pid(Pid) -> Pid; +get_pid(Name) when is_atom(Name) -> whereis(Name). + +job_to_statistic(Job = #job{name = Name}) -> + TZ = get_time_zone(), + Next = get_next_schedule_time(Name), + job_to_statistic(Job, TZ, Next). + +job_to_statistic(Name, State) -> + #state{timer_tab = Timer, job_tab = JobTab, time_zone = TZ} = State, + case ets:lookup(JobTab, Name) of + [Job] -> + Next = get_next_schedule_time(Timer, Name), + {ok, job_to_statistic(Job, TZ, Next)}; + [] -> {error, not_found} + end. + +job_to_statistic(Job, TimeZone, Now) -> + #job{job = JobSpec, status = Status, opts = Opts, + ok = Ok, failed = Failed, result = Res, run_microsecond = RunMs} = Job, + #{start_time := StartTime, end_time := EndTime} = JobSpec, + Start = datetime_to_millisecond(TimeZone, StartTime), + End = datetime_to_millisecond(TimeZone, EndTime), + JobSpec#{status => Status, ok => Ok, failed => Failed, opts => Opts, + next => predict_datetime(Status, JobSpec, Start, End, ?MAX_SIZE, TimeZone, Now), + start_time => to_rfc3339(datetime_to_millisecond(TimeZone, StartTime)), + end_time => to_rfc3339(datetime_to_millisecond(TimeZone, EndTime)), + node => node(), results => Res, run_microsecond => RunMs}. + %% For PropEr Test -ifdef(TEST). -compile(export_all). diff --git a/src/ecron_app.erl b/src/ecron_app.erl index ada95a0..af8d173 100644 --- a/src/ecron_app.erl +++ b/src/ecron_app.erl @@ -10,5 +10,4 @@ start(_StartType, _StartArgs) -> ecron_sup:start_link(). stop(_State) -> - rpc:abcast(nodes(), ?MONITOR_WORKER, {node(), ecron, stop}), ok. diff --git a/src/ecron_monitor.erl b/src/ecron_monitor.erl index 9a1bb34..8ea2acc 100644 --- a/src/ecron_monitor.erl +++ b/src/ecron_monitor.erl @@ -8,17 +8,16 @@ -include("ecron.hrl"). start_link(Name, Jobs) -> - gen_server:start_link(Name, ?MODULE, [Jobs], []). - -init([Jobs]) -> case ecron_tick:parse_crontab(Jobs, []) of - {ok, [_ | _]} -> - erlang:process_flag(trap_exit, true), - ok = net_kernel:monitor_nodes(true, [nodedown_reason]), - {ok, undefined, 25}; - {stop, Reason} -> {stop, Reason} + {ok, [_ | _]} -> gen_server:start_link(Name, ?MODULE, [], []); + {error, Reason} -> {error, Reason} end. +init([]) -> + erlang:process_flag(trap_exit, true), + ok = net_kernel:monitor_nodes(true, [nodedown_reason]), + {ok, undefined, 25}. + handle_call(_Request, _From, State) -> {reply, error, State}. diff --git a/src/ecron_spec.erl b/src/ecron_spec.erl new file mode 100644 index 0000000..67b79bc --- /dev/null +++ b/src/ecron_spec.erl @@ -0,0 +1,286 @@ +%%% @private +-module(ecron_spec). +-include("ecron.hrl"). + +%% API +-export([parse_spec/1]). +-export([parse_crontab/2]). +-export([is_start_end_ok/2]). +-export([parse_valid_opts/1]). + +%% @private +parse_spec("@yearly") -> parse_spec("0 0 1 1 *"); % Run once a year, midnight, Jan. 1st +parse_spec("@annually") -> parse_spec("0 0 1 1 *"); % Same as @yearly +parse_spec("@monthly") -> parse_spec("0 0 1 * *"); % Run once a month, midnight, first of month +parse_spec("@weekly") -> parse_spec("0 0 * * 0"); % Run once a week, midnight between Sat/Sun +parse_spec("@midnight") -> parse_spec("0 0 * * *"); % Run once a day, midnight +parse_spec("@daily") -> parse_spec("0 0 * * *"); % Same as @midnight +parse_spec("@hourly") -> parse_spec("0 * * * *"); % Run once an hour, beginning of hour +parse_spec("@minutely") -> parse_spec("* * * * *"); +parse_spec(Bin) when is_binary(Bin) -> parse_spec(binary_to_list(Bin)); +parse_spec(List) when is_list(List) -> + case string:tokens(string:lowercase(List), " ") of + [_S, _M, _H, _DOM, _Mo, _DOW] = Cron -> parse_cron_spec(Cron); + [_M, _H, _DOM, _Mo, _DOW] = Cron -> parse_cron_spec(["0" | Cron]); + ["@every", Sec] -> parse_every_spec(Sec); + _ -> {error, invalid_spec, List} + end; +parse_spec(Spec) when is_map(Spec) -> + {Months, NewSpec} = take(month, Spec), + case unzip(Months, 1, 12, []) of + {ok, EMonths} -> + List = [{second, 0, 59}, {minute, 0, 59}, {hour, 0, 23}, + {day_of_month, 1, get_max_day_of_months(EMonths)}, + {day_of_week, 0, 6}], + format_map_spec(List, NewSpec, #{month => zip(EMonths)}); + error -> {error, month, Months} + end; +parse_spec(Second) when is_integer(Second) andalso Second =< ?MAX_TIMEOUT -> + {ok, every, Second}; +parse_spec(Spec) -> {error, invalid_spec, Spec}. + +parse_cron_spec([Second, Minute, Hour, DayOfMonth, Month, DayOfWeek]) -> + case parse_field(Month, 1, 12) of + {ok, Months} -> + Fields = [ + {second, Second, 0, 59}, + {minute, Minute, 0, 59}, + {hour, Hour, 0, 23}, + {day_of_month, DayOfMonth, 1, get_max_day_of_months(Months)}, + {day_of_week, DayOfWeek, 0, 6}], + parse_fields(Fields, #{month => Months}); + error -> {error, month, Month} + end. + +parse_fields([], Acc) -> {ok, cron, Acc}; +parse_fields([{Key, Spec, Min, Max} | Rest], Acc) -> + case parse_field(Spec, Min, Max) of + {ok, V} -> parse_fields(Rest, Acc#{Key => V}); + error -> {error, Key, Spec} + end. + +parse_field("*", _Min, _Max) -> {ok, '*'}; +parse_field(Value, MinLimit, MaxLimit) -> + parse_field(string:tokens(Value, ","), MinLimit, MaxLimit, []). + +parse_field([], _MinLimit, _MaxLimit, Acc) -> {ok, zip(lists:usort(Acc))}; +parse_field([Field | Fields], MinL, MaxL, Acc) -> + case string:tokens(Field, "-") of + [Field] -> + case string:tokens(Field, "/") of + [_] -> % Integer + Int = field_to_int(Field), + case Int >= MinL andalso Int =< MaxL of + true -> parse_field(Fields, MinL, MaxL, [Int | Acc]); + false -> error + end; + ["*", StepStr] -> % */Step -> MinLimit~MaxLimit/Step + case field_to_int(StepStr) of + Step when Step > 0 -> + NewAcc = lists:seq(MinL, MaxL, Step) ++ Acc, + parse_field(Fields, MinL, MaxL, NewAcc); + _ -> error + end; + [MinStr, StepStr] -> % Min/Step -> Min~MaxLimit/Step + Min = field_to_int(MinStr), + Step = field_to_int(StepStr), + case Min >= MinL andalso Min =< MaxL andalso Step > 0 of + true -> + NewAcc = lists:seq(Min, MaxL, Step) ++ Acc, + parse_field(Fields, MinL, MaxL, NewAcc); + false -> error + end; + _ -> error + end; + [MinStr, MaxStepStr] -> + case field_to_int(MinStr) of + Min when Min >= MinL andalso Min =< MaxL -> % Min-Max/Step -> Min~Max/Step + {Max, Step} = + case string:tokens(MaxStepStr, "/") of + [_] -> {field_to_int(MaxStepStr), 1}; + [MaxStr, StepStr] -> {field_to_int(MaxStr), field_to_int(StepStr)}; + _ -> {-1, -1} %% error + end, + case Max >= MinL andalso Max >= Min andalso Step > 0 of + true -> + New = lists:seq(Min, Max, Step), + case lists:max(New) =< MaxL of + true -> parse_field(Fields, MinL, MaxL, New ++ Acc); + false -> error + end; + false -> error + end; + _ -> error + end; + _ -> error + end. + +zip('*') -> '*'; +zip([T | Rest]) -> zip(Rest, T + 1, [T], []). + +zip([], _, [Single], Acc) -> lists:reverse([Single | Acc]); +zip([], _, [One, Two], Acc) -> lists:reverse([One, Two | Acc]); +zip([], _, Buffer, Acc) -> lists:reverse([{lists:min(Buffer), lists:max(Buffer)} | Acc]); +zip([L | Rest], L, Buffer, Acc) -> zip(Rest, L + 1, [L | Buffer], Acc); +zip([F | Rest], _Last, [Single], Acc) -> zip(Rest, F + 1, [F], [Single | Acc]); +zip([F | Rest], _Last, [One, Two], Acc) -> zip(Rest, F + 1, [F], [One, Two | Acc]); +zip([F | Rest], _Last, Buffer, Acc) -> + zip(Rest, F + 1, [F], [{lists:min(Buffer), lists:max(Buffer)} | Acc]). + +unzip('*', _MinLimit, _MaxLimit, _Acc) -> {ok, '*'}; +unzip([], _MinLimit, _MaxLimit, Acc) -> {ok, lists:usort(Acc)}; +unzip([{Min, Max} | List], MinL, MaxL, Acc) -> + NewMin = field_to_int(Min), + NewMax = field_to_int(Max), + case NewMax >= NewMin andalso NewMax =< MaxL andalso NewMin >= MinL of + true -> unzip(List, MinL, MaxL, lists:seq(NewMin, NewMax) ++ Acc); + false -> error + end; +unzip([Int | List], MinL, MaxL, Acc) -> + case field_to_int(Int) of + V when V >= MinL andalso V =< MaxL -> unzip(List, MinL, MaxL, [V | Acc]); + _ -> error + end. + + +parse_every_spec(SecSpec) -> + LowerSecSpec = string:lowercase(SecSpec), + List = [{"d", 24 * 3600}, {"h", 3600}, {"m", 60}, {"s", 1}], + case parse_every(List, LowerSecSpec, 0) of + {ok, Sec} when Sec > 0 andalso Sec =< ?MAX_TIMEOUT -> {ok, every, Sec}; + {ok, Sec} -> {error, second, Sec}; + error -> {error, second, SecSpec} + end. + +parse_every(_, "", Sum) -> {ok, Sum}; +parse_every([], _, _Sum) -> error; +parse_every([{Sep, Index} | Rest], Spec, Sum) -> + case parse_every(Spec, Sep) of + {Val, NewSpec} -> parse_every(Rest, NewSpec, Val * Index + Sum); + error -> error + end. + +parse_every(Spec, Seps) -> + case string:tokens(Spec, Seps) of + [Spec] -> {0, Spec}; + [Str, S] -> + case field_to_int(Str) of + Value when Value >= 0 -> {Value, S}; + _ -> error + end; + [Str] -> + case field_to_int(Str) of + Value when Value >= 0 -> {Value, ""}; + _ -> error + end; + _ -> error + end. + +get_max_day_of_months('*') -> 31; +get_max_day_of_months(List) -> max_day_of_months(List, 29). + +max_day_of_months([], Max) -> Max; +max_day_of_months(_, 31) -> 31; +max_day_of_months([{_Min, _Max} | _List], _OldMax) -> 31; %% because Max - Min >= 2 +max_day_of_months([Int | List], Max) -> + NewMax = erlang:max(Max, last_day_of_month(Int)), + max_day_of_months(List, NewMax). + +last_day_of_month(2) -> 29; +last_day_of_month(4) -> 30; +last_day_of_month(6) -> 30; +last_day_of_month(9) -> 30; +last_day_of_month(11) -> 30; +last_day_of_month(M) when is_integer(M), M > 0, M < 13 -> 31. + +-define(Alphabet, #{ + "sun" => 0, "mon" => 1, "tue" => 2, "wed" => 3, "thu" => 4, "fir" => 5, "sat" => 6, + "jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6, + "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12}). + +field_to_int(Int) when is_integer(Int) -> Int; +field_to_int(List) when is_list(List) -> + case maps:find(List, ?Alphabet) of + error -> + case string:list_to_integer(List) of + {Int, []} -> Int; + _ -> -1 %% error + end; + {ok, Int} -> Int + end. + +format_map_spec([], Old, New) when Old =:= #{} -> {ok, cron, New}; +format_map_spec([], Old, _New) -> {error, maps:keys(Old), maps:values(Old)}; +format_map_spec([{Key, Min, Max} | List], Old, New) -> + {Value, Old1} = take(Key, Old), + case unzip(Value, Min, Max, []) of + {ok, EValue} -> format_map_spec(List, Old1, New#{Key => zip(EValue)}); + error -> {error, Key, Value} + end. + +take(Key, Spec) -> + case maps:take(Key, Spec) of + error when Key =:= second -> {[0], Spec}; + error -> {'*', Spec}; + Res -> Res + end. + +parse_crontab([], Acc) -> {ok, Acc}; +parse_crontab([{Name, Spec, {_M, _F, _A} = MFA} | Jobs], Acc) -> + parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, unlimited, unlimited, []} | Jobs], Acc); +parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End} | Jobs], Acc) -> + parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End, []} | Jobs], Acc); +parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End, Opts} | Jobs], Acc) -> + case parse_job(Name, Spec, MFA, Start, End, Opts) of + {ok, Job} -> + case lists:keyfind(Name, #job.name, Acc) of + false -> parse_crontab(Jobs, [Job | Acc]); + _ -> {error, lists:flatten(io_lib:format("Duplicate job name: ~p", [Name]))} + end; + {error, Field, Reason} -> {error, lists:flatten(io_lib:format("~p: ~p", [Field, Reason]))} + end; +parse_crontab([L | _], _Acc) -> {error, L}. + +parse_job(JobName, Spec, MFA, Start, End, Opts) -> + case is_start_end_ok(Start, End) of + true -> + case parse_spec(Spec) of + {ok, Type, Crontab} -> + Job = #{type => Type, name => JobName, crontab => Crontab, mfa => MFA, + start_time => Start, end_time => End}, + {ok, #job{name = JobName, + status = activate, job = Job, + opts = parse_valid_opts(Opts)}}; + ErrParse -> ErrParse + end; + false -> + {error, invalid_time, {Start, End}} + end. + +parse_valid_opts(Opts) -> + Singleton = proplists:get_value(singleton, Opts, true), + MaxCount = proplists:get_value(max_count, Opts, unlimited), + [{singleton, Singleton}, {max_count, MaxCount}]. + +%% @private +is_start_end_ok(Start, End) -> + case is_datetime(Start) andalso is_datetime(End) of + true when Start =/= unlimited andalso End =/= unlimited -> + EndSec = calendar:datetime_to_gregorian_seconds(End), + StartSec = calendar:datetime_to_gregorian_seconds(Start), + EndSec > StartSec; + Res -> Res + end. + +is_datetime(unlimited) -> true; +is_datetime({Date, {H, M, S}}) -> + (is_integer(H) andalso H >= 0 andalso H =< 23) andalso + (is_integer(M) andalso M >= 0 andalso M =< 59) andalso + (is_integer(S) andalso S >= 0 andalso H =< 59) andalso + calendar:valid_date(Date); +is_datetime(_ErrFormat) -> false. + +%% For PropEr Test +-ifdef(TEST). +-compile(export_all). +-endif. diff --git a/src/ecron_sup.erl b/src/ecron_sup.erl index d956b94..3ebd9e0 100644 --- a/src/ecron_sup.erl +++ b/src/ecron_sup.erl @@ -6,7 +6,7 @@ -export([start_global/1, stop_global/1]). -export([start_link/0, init/1]). --define(LOCAL_WORKER, ecron_tick). +-define(LOCAL_WORKER, ecron). -define(GLOBAL_WORKER, ecron_global). start_link() -> @@ -39,7 +39,6 @@ stop_global(Measurements) -> init([]) -> LocalJobs = application:get_env(?Ecron, local_jobs, []), GlobalJobs = application:get_env(?Ecron, global_jobs, []), - ?LocalJob = ets:new(?LocalJob, [named_table, set, public, {keypos, 2}]), SupFlags = #{ strategy => one_for_one, intensity => 100, @@ -53,21 +52,17 @@ init([]) -> type => worker, modules => [?LOCAL_WORKER] }, - Worker = - case GlobalJobs of - [] -> [Local]; - _ -> - ?GlobalJob = ets:new(?GlobalJob, [named_table, set, public, {keypos, 2}]), - [ - Local, - #{ - id => ?MONITOR_WORKER, - start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [?MONITOR_WORKER] - } - ] - end, - {ok, {SupFlags, Worker}}. + case GlobalJobs of + [] -> {ok, {SupFlags, [Local]}}; + _ -> + Global = + #{ + id => ?MONITOR_WORKER, + start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [?MONITOR_WORKER] + }, + {ok, {SupFlags, [Local, Global]}} + end. diff --git a/src/ecron_tick.erl b/src/ecron_tick.erl deleted file mode 100644 index 57c39d3..0000000 --- a/src/ecron_tick.erl +++ /dev/null @@ -1,568 +0,0 @@ -%%% @private --module(ecron_tick). --include("ecron.hrl"). --export([add/2, delete/1]). --export([activate/1, deactivate/1]). --export([statistic/1, statistic/0]). --export([reload/0]). --export([predict_datetime/2, parse_crontab/2]). - --export([start_link/2, handle_call/3, handle_info/2, init/1, handle_cast/2]). --export([spawn_mfa/3, clear/0]). - --record(state, {time_zone, max_timeout, timer_tab, job_tab = ?LocalJob}). --record(job, {name, status = activate, job, opts = [], ok = 0, failed = 0, - link = undefined, result = [], run_microsecond = []}). --record(timer, {key, name, cur_count = 0, singleton, type, spec, mfa, link, - start_sec = unlimited, end_sec = unlimited, max_count = unlimited}). - --define(MAX_SIZE, 16). --define(SECONDS_FROM_0_TO_1970, 719528 * 86400). --define(day_of_week(Y, M, D), (case calendar:day_of_the_week(Y, M, D) of 7 -> 0; D1 -> D1 end)). --define(MatchSpec(Name), [{#timer{name = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Name}}]}]). - -add(Job, Options) -> gen_server:call(?LocalJob, {add, Job, Options}, infinity). -delete(Name) -> gen_server:call(?LocalJob, {delete, Name}, infinity). -activate(Name) -> gen_server:call(?LocalJob, {activate, Name}, infinity). -deactivate(Name) -> gen_server:call(?LocalJob, {deactivate, Name}, infinity). -get_next_schedule_time(Name) -> gen_server:call(?LocalJob, {next_schedule_time, Name}, infinity). -clear() -> gen_server:call(?LocalJob, clear, infinity). -reload() -> gen_server:cast(?LocalJob, reload). - -statistic(Name) -> - case ets:lookup(?LocalJob, Name) of - [Job] -> {ok, job_to_statistic(Job)}; - [] -> - try - gen_server:call({global, ?Ecron}, {statistic, Name}) - catch _:_ -> - {error, not_found} - end - end. - -statistic() -> - Local = ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], ?LocalJob), - Global = - try - gen_server:call({global, ?Ecron}, statistic) - catch _:_ -> - [] - end, - Local ++ Global. - -predict_datetime(Job, Num) -> - TZ = get_time_zone(), - Now = current_millisecond(), - predict_datetime(activate, Job, unlimited, unlimited, Num, TZ, Now). - -start_link(Name, JobSpec) -> - case parse_crontab(JobSpec, []) of - {ok, Jobs} -> gen_server:start_link(Name, ?MODULE, [Name, Jobs], []); - {stop, Reason} -> {error, Reason} - end. - -init([{Type, JobTab}, Jobs]) -> - erlang:process_flag(trap_exit, true), - TZ = get_time_zone(), - Tab = ets:new(ecron_timer, [ordered_set, private, {keypos, #timer.key}]), - MaxTimeout = application:get_env(?Ecron, adjusting_time_second, 7 * 24 * 3600) * 1000, - init(Type, JobTab, Jobs, TZ, MaxTimeout, Tab). - -handle_call({add, Job, Options}, _From, State) -> - #state{time_zone = TZ, timer_tab = Tab, job_tab = JobTab} = State, - {reply, add_job(JobTab, Tab, Job, TZ, Options, false), State, tick(State)}; - -handle_call({delete, Name}, _From, State = #state{timer_tab = Tab, job_tab = JobTab}) -> - delete_job(JobTab, Tab, Name), - {reply, ok, State, next_timeout(State)}; - -handle_call({activate, Name}, _From, State) -> - #state{job_tab = JobTab, time_zone = TZ, timer_tab = TimerTab} = State, - {reply, activate_job(JobTab, Name, TZ, TimerTab), State, tick(State)}; - -handle_call({deactivate, Name}, _From, State) -> - #state{timer_tab = TimerTab, job_tab = JobTab} = State, - {reply, deactivate_job(JobTab, Name, TimerTab), State, next_timeout(State)}; - -handle_call({statistic, Name}, _From, State) -> - Res = job_to_statistic(Name, State), - {reply, Res, State, next_timeout(State)}; - -handle_call(statistic, _From, State = #state{timer_tab = Timer}) -> - Res = - ets:foldl(fun(#timer{name = Name}, Acc) -> - {ok, Item} = job_to_statistic(Name, State), - [Item | Acc] - end, [], Timer), - {reply, Res, State, next_timeout(State)}; - -handle_call({next_schedule_time, Name}, _From, State = #state{timer_tab = Timer}) -> - Reply = get_next_schedule_time(Timer, Name), - {reply, Reply, State, next_timeout(State)}; - -handle_call(clear, _From, State = #state{timer_tab = Timer, job_tab = JobTab}) -> - ets:delete_all_objects(Timer), - ets:delete_all_objects(JobTab), - {reply, ok, State, next_timeout(State)}; - -handle_call(_Unknown, _From, State) -> - {noreply, State, next_timeout(State)}. - -handle_info(timeout, State) -> - {noreply, State, tick(State)}; - -handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> - pid_delete(Pid, TimerTab, JobTab), - {noreply, State, next_timeout(State)}; - -handle_info(_Unknown, State) -> - {noreply, State, next_timeout(State)}. - -handle_cast(_Unknown, State) -> - {noreply, State, next_timeout(State)}. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - -init(local, JobTab, Jobs, TZ, MaxTimeout, Tab) -> - [begin ets:insert_new(JobTab, Job) end || Job <- Jobs], - [begin add_job(JobTab, Tab, Job, TZ, Opts, true) - end || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(JobTab)], - State = #state{max_timeout = MaxTimeout, time_zone = TZ, timer_tab = Tab, job_tab = JobTab}, - {ok, State, next_timeout(State)}; -init(global, JobTab, Jobs, TZ, MaxTimeout, Tab) -> - [begin add_job(JobTab, Tab, Job, TZ, Opts, true) - end || #job{job = Job, opts = Opts, status = activate} <- Jobs], - State = #state{max_timeout = MaxTimeout, time_zone = TZ, timer_tab = Tab, job_tab = JobTab}, - {ok, State, next_timeout(State)}. - -parse_crontab([], Acc) -> {ok, Acc}; -parse_crontab([{Name, Spec, {_M, _F, _A} = MFA} | Jobs], Acc) -> - parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, unlimited, unlimited, []} | Jobs], Acc); -parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End} | Jobs], Acc) -> - parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End, []} | Jobs], Acc); -parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End, Opts} | Jobs], Acc) -> - case parse_job(Name, Spec, MFA, Start, End, Opts) of - {ok, Job} -> - case lists:keyfind(Name, #job.name, Acc) of - false -> parse_crontab(Jobs, [Job | Acc]); - _ -> {stop, lists:flatten(io_lib:format("Duplicate job name: ~p", [Name]))} - end; - {error, Field, Reason} -> {stop, lists:flatten(io_lib:format("~p: ~p", [Field, Reason]))} - end; -parse_crontab([L | _], _Acc) -> {stop, L}. - -parse_job(JobName, Spec, MFA, Start, End, Opts) -> - case ecron:valid_datetime(Start, End) of - true -> - case ecron:parse_spec(Spec) of - {ok, Type, Crontab} -> - Job = #{type => Type, name => JobName, crontab => Crontab, mfa => MFA, - start_time => Start, end_time => End}, - {ok, #job{name = JobName, - status = activate, job = Job, - opts = valid_opts(Opts), - link = link_pid(MFA)}}; - ErrParse -> ErrParse - end; - false -> - {error, invalid_time, {Start, End}} - end. - -add_job(JobTab, Tab, #{name := Name, mfa := MFA} = Job, TZ, Opts, IsNewJob) -> - NewOpts = valid_opts(Opts), - Pid = link_pid(MFA), - JobRec = #job{status = activate, name = Name, job = Job, opts = NewOpts, link = Pid}, - Insert = ets:insert_new(JobTab, JobRec), - Now = current_millisecond(), - telemetry:execute(?Activate, #{action_ms => Now}, #{name => Name, mfa => MFA}), - update_timer(Insert orelse IsNewJob, TZ, NewOpts, Job, Now, Pid, Tab, JobTab). - -activate_job(JobTab, Name, TZ, Tab) -> - case ets:lookup(JobTab, Name) of - [] -> {error, not_found}; - [#job{job = Job, opts = Opts}] -> - delete_job(JobTab, Tab, Name), - case add_job(JobTab, Tab, Job, TZ, Opts, false) of - {ok, Name} -> ok; - Err -> Err - end - end. - -deactivate_job(JobTab, Name, Timer) -> - ets:select_delete(Timer, ?MatchSpec(Name)), - case ets:update_element(JobTab, Name, {#job.status, deactivate}) of - true -> - telemetry:execute(?Deactivate, #{action_ms => current_millisecond()}, #{name => Name}), - ok; - false -> {error, not_found} - end. - -delete_job(JobTab, TimerTab, Name) -> - ets:select_delete(TimerTab, ?MatchSpec(Name)), - telemetry:execute(?Delete, #{action_ms => current_millisecond()}, #{name => Name}), - case ets:lookup(JobTab, Name) of - [] -> ok; - [#job{link = Link}] -> - unlink_pid(Link), - ets:delete(JobTab, Name) - end. - -update_timer(false, _, _, _, _, _, _, _) -> {error, already_exist}; -update_timer(true, TZ, Opts, Job, Now, LinkPid, Tab, JobTab) -> - Singleton = proplists:get_value(singleton, Opts), - MaxCount = proplists:get_value(max_count, Opts), - #{name := Name, crontab := Spec, type := Type, mfa := MFA, - start_time := StartTime, end_time := EndTime} = Job, - Start = datetime_to_millisecond(TZ, StartTime), - End = datetime_to_millisecond(TZ, EndTime), - case next_schedule_millisecond(Type, Spec, TZ, Now, Start, End) of - {ok, NextSec} -> - Timer = #timer{key = {NextSec, Name}, singleton = Singleton, - name = Name, type = Type, spec = Spec, max_count = MaxCount, - mfa = MFA, start_sec = Start, end_sec = End, link = LinkPid}, - ets:insert(Tab, Timer), - {ok, Name}; - {error, already_ended} = Err -> - delete_job(JobTab, Tab, Name), - Err - end. - -current_millisecond() -> erlang:system_time(millisecond). - -datetime_to_millisecond(_, unlimited) -> unlimited; -datetime_to_millisecond(local, DateTime) -> - UtcTime = erlang:localtime_to_universaltime(DateTime), - datetime_to_millisecond(utc, UtcTime); -datetime_to_millisecond(utc, DateTime) -> - (calendar:datetime_to_gregorian_seconds(DateTime) - ?SECONDS_FROM_0_TO_1970) * 1000. - -millisecond_to_datetime(local, Ms) -> calendar:system_time_to_local_time(Ms, millisecond); -millisecond_to_datetime(utc, Ms) -> calendar:system_time_to_universal_time(Ms, millisecond). - -spawn_mfa(JobTab, Name, MFA) -> - Start = erlang:monotonic_time(), - {Event, OkInc, FailedInc, NewRes} = - try - case MFA of - {erlang, send, [Pid, Message]} -> - erlang:send(Pid, Message), - {?Success, 1, 0, ok}; - {M, F, A} -> {?Success, 1, 0, apply(M, F, A)}; - {F, A} -> {?Success, 1, 0, apply(F, A)} - end - catch - Error:Reason:Stacktrace -> - {?Failure, 0, 1, {Error, Reason, Stacktrace}} - end, - End = erlang:monotonic_time(), - Cost = erlang:convert_time_unit(End - Start, native, microsecond), - telemetry:execute(Event, #{run_microsecond => Cost, run_result => NewRes}, #{name => Name, mfa => MFA}), - case ets:lookup(JobTab, Name) of - [] -> ok; - [Job] -> - #job{ok = Ok, failed = Failed, run_microsecond = RunMs, result = Results} = Job, - Elements = [{#job.ok, Ok + OkInc}, {#job.failed, Failed + FailedInc}, - {#job.run_microsecond, lists:sublist([Cost | RunMs], ?MAX_SIZE)}, - {#job.result, lists:sublist([NewRes | Results], ?MAX_SIZE)}], - ets:update_element(JobTab, Name, Elements) - end. - -tick(State = #state{timer_tab = TimerTab}) -> - tick_tick(ets:first(TimerTab), current_millisecond(), State). - -tick_tick('$end_of_table', _Cur, _State) -> infinity; -tick_tick({Due, _Name}, Cur, #state{max_timeout = MaxTimeout}) when Due > Cur -> - min(Due - Cur, MaxTimeout); -tick_tick(Key = {Due, Name}, Cur, State) -> - #state{time_zone = TZ, timer_tab = TimerTab, job_tab = JobTab} = State, - [Cron = #timer{singleton = Singleton, mfa = MFA, max_count = MaxCount, cur_count = CurCount}] = ets:lookup(TimerTab, Key), - ets:delete(TimerTab, Key), - {Incr, CurPid} = maybe_spawn_worker(Cur - Due < 1000, Singleton, Name, MFA, JobTab), - update_next_schedule(CurCount + Incr, MaxCount, Cron, Cur, Name, TZ, CurPid, TimerTab, JobTab), - tick(State). - -update_next_schedule(Max, Max, _Cron, _Cur, Name, _TZ, _CurPid, Tab, JobTab) -> delete_job(JobTab, Tab, Name); -update_next_schedule(Count, _Max, Cron, Cur, Name, TZ, CurPid, Tab, JobTab) -> - #timer{type = Type, start_sec = Start, end_sec = End, spec = Spec} = Cron, - case next_schedule_millisecond(Type, Spec, TZ, Cur, Start, End) of - {ok, Next} -> - NextTimer = Cron#timer{key = {Next, Name}, singleton = CurPid, cur_count = Count}, - ets:insert(Tab, NextTimer); - {error, already_ended} -> - delete_job(JobTab, Tab, Name) - end. - -next_schedule_millisecond(every, Sec, _TimeZone, Now, Start, End) -> - Next = Now + Sec * 1000, - case in_range(Next, Start, End) of - {error, deactivate} -> {ok, Start}; - {error, already_ended} -> {error, already_ended}; - ok -> {ok, Next} - end; -next_schedule_millisecond(cron, Spec, TimeZone, Now, Start, End) -> - ForwardDateTime = millisecond_to_datetime(TimeZone, Now + 1000), - DefaultMin = #{second => 0, minute => 0, hour => 0, - day_of_month => 1, month => 1, day_of_week => 0}, - Min = spec_min(maps:to_list(Spec), DefaultMin), - NextDateTime = next_schedule_datetime(Spec, Min, ForwardDateTime), - Next = datetime_to_millisecond(TimeZone, NextDateTime), - case in_range(Next, Start, End) of - {error, deactivate} -> next_schedule_millisecond(cron, Spec, TimeZone, Start, Start, End); - {error, already_ended} -> {error, already_ended}; - ok -> {ok, Next} - end. - -next_schedule_datetime(DateSpec, Min, DateTime) -> - #{ - second := SecondSpec, minute := MinuteSpec, hour := HourSpec, - day_of_month := DayOfMonthSpec, month := MonthSpec, - day_of_week := DayOfWeekSpec} = DateSpec, - {{Year, Month, Day}, {Hour, Minute, Second}} = DateTime, - case valid_datetime(MonthSpec, Month) of - false -> forward_month(DateTime, Min, DateSpec); - true -> - case valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) of - false -> - LastDay = calendar:last_day_of_the_month(Year, Month), - forward_day(DateTime, Min, LastDay, DateSpec); - true -> - case valid_datetime(HourSpec, Hour) of - false -> forward_hour(DateTime, Min, DateSpec); - true -> - case valid_datetime(MinuteSpec, Minute) of - false -> forward_minute(DateTime, Min, DateSpec); - true -> - case valid_datetime(SecondSpec, Second) of - false -> forward_second(DateTime, Min, DateSpec); - true -> DateTime - end - end - end - end - end. - -forward_second(DateTime, Min, Spec) -> - {{Year, Month, Day}, {Hour, Minute, Second}} = DateTime, - NewSecond = nearest(second, Second, 59, Spec), - case Second >= NewSecond of - true -> forward_minute(DateTime, Min, Spec); - false -> {{Year, Month, Day}, {Hour, Minute, NewSecond}} - end. - -forward_minute(DateTime, Min, Spec) -> - {{Year, Month, Day}, {Hour, Minute, _Second}} = DateTime, - NewMinute = nearest(minute, Minute, 59, Spec), - case Minute >= NewMinute of - true -> forward_hour(DateTime, Min, Spec); - false -> - #{second := SecondM} = Min, - {{Year, Month, Day}, {Hour, NewMinute, SecondM}} - end. - -forward_hour(DateTime, Min, Spec) -> - {{Year, Month, Day}, {Hour, _Minute, _Second}} = DateTime, - NewHour = nearest(hour, Hour, 23, Spec), - case Hour >= NewHour of - true -> - LastDay = calendar:last_day_of_the_month(Year, Month), - forward_day(DateTime, Min, LastDay, Spec); - false -> - #{minute := MinuteM, second := SecondM} = Min, - {{Year, Month, Day}, {NewHour, MinuteM, SecondM}} - end. - -forward_day(DateTime, Min, LastDay, Spec) -> - {{Year, Month, Day}, {_Hour, _Minute, _Second}} = DateTime, - case Day + 1 of - NewDay when NewDay > LastDay -> forward_month(DateTime, Min, Spec); - NewDay -> - #{hour := HourM, minute := MinuteM, second := SecondM} = Min, - NewDateTime = {{Year, Month, NewDay}, {HourM, MinuteM, SecondM}}, - #{day_of_week := DayOfWeekSpec, day_of_month := DayOfMonthSpec} = Spec, - case valid_day(Year, Month, NewDay, DayOfMonthSpec, DayOfWeekSpec) of - true -> NewDateTime; - false -> forward_day(NewDateTime, Min, LastDay, Spec) - end - end. - -forward_month(DateTime, Min, Spec) -> - {{Year, Month, _Day}, {_Hour, _Minute, _Second}} = DateTime, - NewMonth = nearest(month, Month, 12, Spec), - #{month := MonthM, hour := HourM, minute := MinuteM, second := SecondM} = Min, - NewDateTime = - {{NYear, NMonth, NDay}, {_NHour, _NMinute, _NSecond}} = - case Month >= NewMonth of - true -> {{Year + 1, MonthM, 1}, {HourM, MinuteM, SecondM}}; - false -> {{Year, NewMonth, 1}, {HourM, MinuteM, SecondM}} - end, - #{day_of_week := DayOfWeekSpec, day_of_month := DayOfMonthSpec} = Spec, - case valid_day(NYear, NMonth, NDay, DayOfMonthSpec, DayOfWeekSpec) of - false -> - LastDay = calendar:last_day_of_the_month(NYear, NMonth), - forward_day(NewDateTime, Min, LastDay, Spec); - true -> NewDateTime - end. - -nearest(Type, Current, Max, Spec) -> - Values = maps:get(Type, Spec), - nearest_1(Values, Values, Max, Current + 1). - -nearest_1('*', '*', MaxLimit, Next) when Next > MaxLimit -> 1; -nearest_1('*', '*', _MaxLimit, Next) -> Next; -nearest_1([], [{Min, _} | _], _Max, _Next) -> Min; -nearest_1([], [Min | _], _Max, _Next) -> Min; -nearest_1([{Min, Max} | Rest], Spec, MaxLimit, Next) -> - if - Next > Max -> nearest_1(Rest, Spec, MaxLimit, Next); - Next =< Min -> Min; - true -> Next - end; -nearest_1([Expect | Rest], Spec, MaxLimit, Next) -> - case Next > Expect of - true -> nearest_1(Rest, Spec, MaxLimit, Next); - false -> Expect - end. - -valid_datetime('*', _Value) -> true; -valid_datetime([], _Value) -> false; -valid_datetime([Value | _T], Value) -> true; -valid_datetime([{Lower, Upper} | _], Value) when Lower =< Value andalso Value =< Upper -> true; -valid_datetime([_ | T], Value) -> valid_datetime(T, Value). - -valid_day(_Year, _Month, _Day, '*', '*') -> true; -valid_day(_Year, _Month, Day, DayOfMonthSpec, '*') -> - valid_datetime(DayOfMonthSpec, Day); -valid_day(Year, Month, Day, '*', DayOfWeekSpec) -> - DayOfWeek = ?day_of_week(Year, Month, Day), - valid_datetime(DayOfWeekSpec, DayOfWeek); -valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) -> - case valid_datetime(DayOfMonthSpec, Day) of - false -> - DayOfWeek = ?day_of_week(Year, Month, Day), - valid_datetime(DayOfWeekSpec, DayOfWeek); - true -> true - end. - -spec_min([], Acc) -> Acc; -spec_min([{Key, Value} | Rest], Acc) -> - NewAcc = - case Value of - '*' -> Acc; - [{Min, _} | _] -> Acc#{Key => Min}; - [Min | _] -> Acc#{Key => Min} - end, - spec_min(Rest, NewAcc). - -next_timeout(#state{max_timeout = MaxTimeout, timer_tab = TimerTab}) -> - case ets:first(TimerTab) of - '$end_of_table' -> infinity; - {Due, _} -> min(max(Due - current_millisecond(), 0), MaxTimeout) - end. - -in_range(_Current, unlimited, unlimited) -> ok; -in_range(Current, unlimited, End) when Current > End -> {error, already_ended}; -in_range(_Current, unlimited, _End) -> ok; -in_range(Current, Start, unlimited) when Current < Start -> {error, deactivate}; -in_range(_Current, _Start, unlimited) -> ok; -in_range(Current, _Start, End) when Current > End -> {error, already_ended}; -in_range(Current, Start, _End) when Current < Start -> {error, deactivate}; -in_range(_Current, _Start, _End) -> ok. - -to_rfc3339(unlimited) -> unlimited; -to_rfc3339(Next) -> calendar:system_time_to_rfc3339(Next div 1000, [{unit, second}]). - -predict_datetime(deactivate, _, _, _, _, _, _) -> []; -predict_datetime(activate, #{type := every, crontab := Sec} = Job, Start, End, Num, TimeZone, NowT) -> - Now = case maps:find(name, Job) of error -> NowT; _ -> NowT - Sec * 1000 end, - predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []); -predict_datetime(activate, Job, Start, End, Num, TimeZone, Now) -> - predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []). - -predict_datetime_2(_Job, _TimeZone, _Now, _Start, _End, 0, Acc) -> lists:reverse(Acc); -predict_datetime_2(Job, TimeZone, Now, Start, End, Num, Acc) -> - #{type := Type, crontab := Spec} = Job, - case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of - {ok, Next} -> - NewAcc = [to_rfc3339(Next) | Acc], - predict_datetime_2(Job, TimeZone, Next, Start, End, Num - 1, NewAcc); - {error, already_ended} -> lists:reverse(Acc) - end. - -get_next_schedule_time(Timer, Name) -> - %% P = ets:fun2ms(fun(#timer{name = N, key = {Time, _}}) when N =:= Name -> Time end), - P = [{#timer{key = {'$1', '_'}, name = '$2', _ = '_'}, [{'=:=', '$2', {const, Name}}], ['$1']}], - case ets:select(Timer, P) of - [T] -> T; - [] -> current_millisecond() - end. - -get_time_zone() -> application:get_env(?Ecron, time_zone, local). - -maybe_spawn_worker(true, _, Name, {erlang, send, Args}, JobTab) -> - {1, spawn_mfa(JobTab, Name, {erlang, send, Args})}; -maybe_spawn_worker(true, true, Name, MFA, JobTab) -> - {1, spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA])}; -maybe_spawn_worker(true, false, Name, MFA, JobTab) -> - spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA]), - {1, false}; -maybe_spawn_worker(true, Pid, Name, MFA, JobTab) when is_pid(Pid) -> - case is_process_alive(Pid) of - true -> {0, Pid}; - false -> {1, spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA])} - end; -maybe_spawn_worker(false, Singleton, _Name, _MFA, _JobTab) -> {0, Singleton}. - -pid_delete(Pid, TimerTab, JobTab) -> - TimerMatch = [{#timer{link = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Pid}}]}], - JobMatch = [{#job{link = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Pid}}]}], - ets:select_delete(TimerTab, TimerMatch), - ets:select_delete(JobTab, JobMatch). - -valid_opts(Opts) -> - Singleton = proplists:get_value(singleton, Opts, true), - MaxCount = proplists:get_value(max_count, Opts, unlimited), - [{singleton, Singleton}, {max_count, MaxCount}]. -link_pid({erlang, send, [PidOrName, _Message]}) -> - Pid = get_pid(PidOrName), - is_pid(Pid) andalso (catch link(Pid)), - Pid; -link_pid(_MFA) -> undefined. - -unlink_pid(Pid) when is_pid(Pid) -> catch unlink(Pid); -unlink_pid(_) -> ok. - -get_pid(Pid) when is_pid(Pid) -> Pid; -get_pid(Name) when is_atom(Name) -> whereis(Name). - -job_to_statistic(Job = #job{name = Name}) -> - TZ = get_time_zone(), - Next = get_next_schedule_time(Name), - job_to_statistic(Job, TZ, Next). - -job_to_statistic(Name, State) -> - #state{timer_tab = Timer, job_tab = JobTab, time_zone = TZ} = State, - case ets:lookup(JobTab, Name) of - [Job] -> - Next = get_next_schedule_time(Timer, Name), - {ok, job_to_statistic(Job, TZ, Next)}; - [] -> {error, not_found} - end. - -job_to_statistic(Job, TimeZone, Now) -> - #job{job = JobSpec, status = Status, opts = Opts, - ok = Ok, failed = Failed, result = Res, run_microsecond = RunMs} = Job, - #{start_time := StartTime, end_time := EndTime} = JobSpec, - Start = datetime_to_millisecond(TimeZone, StartTime), - End = datetime_to_millisecond(TimeZone, EndTime), - JobSpec#{status => Status, ok => Ok, failed => Failed, opts => Opts, - next => predict_datetime(Status, JobSpec, Start, End, ?MAX_SIZE, TimeZone, Now), - start_time => to_rfc3339(datetime_to_millisecond(TimeZone, StartTime)), - end_time => to_rfc3339(datetime_to_millisecond(TimeZone, EndTime)), - node => node(), results => Res, run_microsecond => RunMs}. - -%% For PropEr Test --ifdef(TEST). --compile(export_all). --endif. diff --git a/test/prop_ecron.erl b/test/prop_ecron.erl index 923900f..cea095c 100644 --- a/test/prop_ecron.erl +++ b/test/prop_ecron.erl @@ -70,15 +70,15 @@ check_day_of_month([_S, _M, _H, _DOM, "*", _DOW]) -> true; check_day_of_month([_S, _M, _H, DOM, Month, _DOW]) -> DOMExtend = unzip(field_to_extend(DOM)), MonthExtend = unzip(field_to_extend(Month)), - ecron:get_max_day_of_months(MonthExtend) >= lists:max(DOMExtend). + ecron_spec:get_max_day_of_months(MonthExtend) >= lists:max(DOMExtend). field_to_extend("*") -> '*'; -field_to_extend({'*/Step', _Type, MinLimit, MaxLimit, Step}) -> ecron:zip(lists:seq(MinLimit, MaxLimit, Step)); +field_to_extend({'*/Step', _Type, MinLimit, MaxLimit, Step}) -> ecron_spec:zip(lists:seq(MinLimit, MaxLimit, Step)); field_to_extend({'Integer', _Type, Int}) -> [Int]; -field_to_extend({'Min-Max', _Type, Min, Max}) -> ecron:zip(lists:seq(Min, Max)); -field_to_extend({'Min/Step', _Type, Min, MaxLimit, Step}) -> ecron:zip(lists:seq(Min, MaxLimit, Step)); -field_to_extend({'Min-Max/Step', _Type, Min, Max, Step}) -> ecron:zip(lists:seq(Min, Max, Step)); -field_to_extend({list, _Type, List}) -> ecron:zip(unzip(lists:flatten([field_to_extend(L) || L <- List]))). +field_to_extend({'Min-Max', _Type, Min, Max}) -> ecron_spec:zip(lists:seq(Min, Max)); +field_to_extend({'Min/Step', _Type, Min, MaxLimit, Step}) -> ecron_spec:zip(lists:seq(Min, MaxLimit, Step)); +field_to_extend({'Min-Max/Step', _Type, Min, Max, Step}) -> ecron_spec:zip(lists:seq(Min, Max, Step)); +field_to_extend({list, _Type, List}) -> ecron_spec:zip(unzip(lists:flatten([field_to_extend(L) || L <- List]))). to_ms(unlimited) -> unlimited; to_ms(Time) -> calendar:rfc3339_to_system_time(Time, [{unit, millisecond}]). diff --git a/test/prop_ecron_app_config.erl b/test/prop_ecron_app_config.erl index 28c9666..fb16ac1 100644 --- a/test/prop_ecron_app_config.erl +++ b/test/prop_ecron_app_config.erl @@ -70,7 +70,7 @@ prop_application_error_config() -> end). check_spec(Actual, SpecStr) -> - {error, Field, Reason} = ecron:parse_spec(SpecStr), + {error, Field, Reason} = ecron_spec:parse_spec(SpecStr), Expect = lists:flatten(io_lib:format("~p: ~p", [Field, Reason])), Actual =:= Expect. \ No newline at end of file diff --git a/test/prop_ecron_global_SUITE.erl b/test/prop_ecron_global_SUITE.erl index 9b0c5b9..80fa4d0 100644 --- a/test/prop_ecron_global_SUITE.erl +++ b/test/prop_ecron_global_SUITE.erl @@ -35,79 +35,79 @@ end_per_suite(_Config) -> basic(_Config) -> start_master(2), - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), start_slave(?Slave1, 2), timer:sleep(300), - Pid = global:whereis_name(ecron), + Pid = global:whereis_name(?GlobalJob), true = is_pid(Pid), timer:sleep(300), - Pid1 = rpc:call(?Slave1, global, whereis_name, [ecron]), + Pid1 = rpc:call(?Slave1, global, whereis_name, [?GlobalJob]), true = is_pid(Pid1), start_slave(?Slave2, 2), timer:sleep(300), - Pid2 = rpc:call(?Slave2, global, whereis_name, [ecron]), + Pid2 = rpc:call(?Slave2, global, whereis_name, [?GlobalJob]), true = is_pid(Pid2), stop_slave(?Slave1), timer:sleep(300), - Pid3 = global:whereis_name(ecron), + Pid3 = global:whereis_name(?GlobalJob), true = is_pid(Pid3), stop_slave(?Slave2), timer:sleep(300), - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), stop_master(), - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), ok. quorum(_Config) -> start_master(1), timer:sleep(300), - Pid0 = global:whereis_name(ecron), + Pid0 = global:whereis_name(?GlobalJob), error = gen_server:call(ecron_monitor, test), gen_server:cast(ecron_monitor, test), true = is_pid(Pid0), start_slave(?Slave1, 1), - Pid = global:whereis_name(ecron), + Pid = global:whereis_name(?GlobalJob), true = is_pid(Pid), timer:sleep(300), - Pid1 = rpc:call(?Slave1, global, whereis_name, [ecron]), + Pid1 = rpc:call(?Slave1, global, whereis_name, [?GlobalJob]), true = is_pid(Pid1), start_slave(?Slave2, 1), timer:sleep(300), - Pid2 = rpc:call(?Slave2, global, whereis_name, [ecron]), + Pid2 = rpc:call(?Slave2, global, whereis_name, [?GlobalJob]), true = is_pid(Pid2), stop_slave(?Slave1), timer:sleep(300), - Pid3 = global:whereis_name(ecron), + Pid3 = global:whereis_name(?GlobalJob), true = is_pid(Pid3), stop_slave(?Slave2), timer:sleep(300), - Pid4 = global:whereis_name(ecron), + Pid4 = global:whereis_name(?GlobalJob), true = is_pid(Pid4), stop_master(), - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), ok. quorum_in_majority(_Config) -> start_master(2), timer:sleep(300), - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), start_slave(?Slave1, 2), timer:sleep(300), - Pid = global:whereis_name(ecron), + Pid = global:whereis_name(?GlobalJob), true = is_pid(Pid), - Pid1 = rpc:call(?Slave1, global, whereis_name, [ecron]), + Pid1 = rpc:call(?Slave1, global, whereis_name, [?GlobalJob]), true = is_pid(Pid1), start_slave(?Slave2, 2), timer:sleep(300), - Pid2 = rpc:call(?Slave2, global, whereis_name, [ecron]), + Pid2 = rpc:call(?Slave2, global, whereis_name, [?GlobalJob]), true = is_pid(Pid2), rpc:call(?Slave1, application, stop, [ecron]), timer:sleep(300), - Pid3 = global:whereis_name(ecron), + Pid3 = global:whereis_name(?GlobalJob), true = is_pid(Pid3), rpc:call(?Slave2, application, stop, [ecron]), timer:sleep(300), - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), stop_slave(?Slave1), stop_slave(?Slave2), stop_master(), @@ -119,12 +119,12 @@ transfer(_Config) -> start_slave(?Slave2, 1), stop_master(), timer:sleep(300), - Pid1 = global:whereis_name(ecron), + Pid1 = global:whereis_name(?GlobalJob), true = is_pid(Pid1), start_master(1), {ok, #{node := Node1}} = ecron:statistic(global_job), ok = rpc:call(Node1, application, stop, [ecron]), - Pid2 = global:whereis_name(ecron), + Pid2 = global:whereis_name(?GlobalJob), true = is_pid(Pid2), true = (Pid2 =/= Pid1), {error, not_found} = ecron:statistic(no_found), @@ -143,7 +143,7 @@ error_config(_Config) -> {failed_to_start_child, ecron_monitor, "invalid_spec: \"* */10 * * * * *\""}}, {ecron_app, start, [normal, []]}}} = Reason, - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), ok. duplicate_config(_Config) -> @@ -157,7 +157,7 @@ duplicate_config(_Config) -> {failed_to_start_child, ecron_monitor, "Duplicate job name: global_job"}}, {ecron_app, start, [normal, []]}}} = Reason, - undefined = global:whereis_name(ecron), + undefined = global:whereis_name(?GlobalJob), ok. -define(Env(Quorum), [ diff --git a/test/prop_ecron_parse.erl b/test/prop_ecron_parse.erl index 3aaca38..960a2ea 100644 --- a/test/prop_ecron_parse.erl +++ b/test/prop_ecron_parse.erl @@ -21,26 +21,26 @@ prop_zip(doc) -> "ecron:zip/1 failed"; prop_zip(opts) -> [{numtests, 4000}]. prop_zip() -> ?FORALL(List, usort_list(non_neg_integer()), - prop_ecron:unzip(ecron:zip(List)) =:= List). + prop_ecron:unzip(ecron_spec:zip(List)) =:= List). prop_max_day_of_months(doc) -> "ecron:get_max_day_of_months/1 failed"; prop_max_day_of_months(opts) -> [{numtests, 1000}]. prop_max_day_of_months() -> ?FORALL(List, usort_list(prop_ecron_spec:month()), begin - ZipList = ecron:zip(List), - Actual = ecron:get_max_day_of_months(ZipList), + ZipList = ecron_spec:zip(List), + Actual = ecron_spec:get_max_day_of_months(ZipList), Expect = lists:max(lists:map(fun prop_ecron:max_day_of_month/1, List)), Expect =:= Actual end). -prop_valid_datetime(doc) -> "ecron:valid_datetime/2 failed"; +prop_valid_datetime(doc) -> "ecron_spec:is_start_end_ok/2 failed"; prop_valid_datetime(opts) -> [{numtests, 4000}]. prop_valid_datetime() -> ?FORALL({Date, Time}, prop_ecron_spec:datetime(), - ?IMPLIES(calendar:valid_date(Date), ecron:valid_datetime({Date, Time}))). + ?IMPLIES(calendar:valid_date(Date), ecron_spec:is_datetime({Date, Time}))). -prop_spec_extend(doc) -> "ecron:parse_spec(\"second minute hour day_of_month month day_of_week\") failed"; +prop_spec_extend(doc) -> "ecron_spec:parse_spec(\"second minute hour day_of_month month day_of_week\") failed"; prop_spec_extend(opts) -> [{numtests, 4000}]. prop_spec_extend() -> ?FORALL(Spec, prop_ecron_spec:extend_spec(), @@ -56,14 +56,14 @@ prop_spec_extend() -> end) ). -prop_spec_standard(doc) -> "ecron:parse_spec(\"minute hour day_of_month month day_of_week\") failed"; +prop_spec_standard(doc) -> "ecron_spec:parse_spec(\"minute hour day_of_month month day_of_week\") failed"; prop_spec_standard(opts) -> [{numtests, 4000}]. prop_spec_standard() -> ?FORALL(Spec, prop_ecron_spec:standard_spec(), ?IMPLIES(prop_ecron:check_day_of_month(Spec), begin SpecStr = prop_ecron:spec_to_str(Spec), - Actual = ecron:parse_spec(SpecStr), + Actual = ecron_spec:parse_spec(SpecStr), Expect = spec_to_extend(Spec), ?WHENFAIL( io:format("Parse ~s Failed: ~p\n ~p\n", [SpecStr, Expect, Actual]), @@ -74,12 +74,12 @@ prop_spec_standard() -> -define(PredefinedSpec, ["@yearly", "@annually", "@monthly", "@weekly", "@midnight", "@daily", "@hourly", "@minutely"]). -prop_spec_predefined(doc) -> "ecron:parse_spec/1 predefined spec failed"; +prop_spec_predefined(doc) -> "ecron_spec:parse_spec/1 predefined spec failed"; prop_spec_predefined(opts) -> [{numtests, 2000}]. prop_spec_predefined() -> ?FORALL(Spec, elements(?PredefinedSpec), begin - Actual = ecron:parse_spec(Spec), + Actual = ecron_spec:parse_spec(Spec), Expect = spec_to_extend(Spec), ?WHENFAIL( io:format("Parse ~s SpecFailed: ~p\n ~p\n", [Spec, Expect, Actual]), @@ -87,7 +87,7 @@ prop_spec_predefined() -> ) end). -prop_spec_every(doc) -> "ecron:parse_spec/1 @every 1d2h3m4s spec failed"; +prop_spec_every(doc) -> "ecron_spec:parse_spec/1 @every 1d2h3m4s spec failed"; prop_spec_every(opts) -> [{numtests, 4000}]. prop_spec_every() -> ?FORALL(Spec, every_spec(), @@ -95,7 +95,7 @@ prop_spec_every() -> begin {Format, Args} = every_spec_to_str(Spec), SpecStr = lists:flatten(io_lib:format("@every " ++ Format, Args)), - Actual = ecron:parse_spec(SpecStr), + Actual = ecron_spec:parse_spec(SpecStr), Expect = {ok, every, every_spec_to_second(Spec)}, ?WHENFAIL( io:format("Parse ~s Failed: ~p\n~p\n", [SpecStr, Expect, Actual]), @@ -104,7 +104,7 @@ prop_spec_every() -> end) ). -prop_spec_every_error(doc) -> "ecron:parse_spec/1 @every 1d2h3m4s spec failed"; +prop_spec_every_error(doc) -> "ecron_spec:parse_spec/1 @every 1d2h3m4s spec failed"; prop_spec_every_error(opts) -> [{numtests, 4000}]. prop_spec_every_error() -> ?FORALL(Spec, every_error_spec(), @@ -118,7 +118,7 @@ prop_spec_every_error() -> ) end). -prop_maybe_error_spec(doc) -> "ecron:parse_spec/1 error spec failed"; +prop_maybe_error_spec(doc) -> "ecron_spec:parse_spec/1 error spec failed"; prop_maybe_error_spec(opts) -> [{numtests, 4000}]. prop_maybe_error_spec() -> ?FORALL(Spec, prop_ecron_spec:maybe_error_spec(), @@ -161,7 +161,7 @@ prop_map_error_spec() -> second -> maps:put(Extra, [0], Expect); _ -> maps:put(Extra, '*', Expect) end, - case ecron:parse_spec(SpecMap) of + case ecron_spec:parse_spec(SpecMap) of {ok, cron, Ecron} -> ?WHENFAIL( io:format("Parse ~p\n Failed: ~p\n ~p\n", [SpecMap, NewExpect, Ecron]), @@ -175,7 +175,7 @@ prop_map_error_spec() -> end end). -prop_error_format(doc) -> "ecron:parse_spec/1 spec wrong format failed"; +prop_error_format(doc) -> "ecron_spec:parse_spec/1 spec wrong format failed"; prop_error_format(opts) -> [{numtests, 2000}]. prop_error_format() -> ?FORALL({Spec, Rand, Extra}, diff --git a/test/prop_ecron_predict_datetime.erl b/test/prop_ecron_predict_datetime.erl index 4c1bfe9..5ebfc7d 100644 --- a/test/prop_ecron_predict_datetime.erl +++ b/test/prop_ecron_predict_datetime.erl @@ -17,7 +17,7 @@ prop_predict_cron_datetime() -> begin {CrontabSpec, StartShift, EndShift} = Spec, SpecStr = prop_ecron:spec_to_str(CrontabSpec), - {ok, cron, NewCrontabSpec} = ecron:parse_spec(SpecStr), + {ok, cron, NewCrontabSpec} = ecron_spec:parse_spec(SpecStr), Now = erlang:system_time(millisecond), StartTime = shift_time(Now, StartShift), EndTime = shift_time(Now, EndShift), @@ -27,9 +27,9 @@ prop_predict_cron_datetime() -> start_time => StartTime, end_time => EndTime }, - Start = ecron_tick:datetime_to_millisecond(local, StartTime), - End = ecron_tick:datetime_to_millisecond(local, EndTime), - List = ecron_tick:predict_datetime(activate, NewSpec, Start, End, 500, local, Now), + Start = ecron:datetime_to_millisecond(local, StartTime), + End = ecron:datetime_to_millisecond(local, EndTime), + List = ecron:predict_datetime(activate, NewSpec, Start, End, 500, local, Now), NowDateTime = calendar:system_time_to_local_time(Now, millisecond), ExpectList = predict_cron_datetime(StartTime, EndTime, NewCrontabSpec, {local, NowDateTime}, 500, []), ?WHENFAIL( @@ -57,7 +57,7 @@ prop_predict_every_datetime() -> }, Start = shift_ms(Now, StartShift), End = shift_ms(Now, EndShift), - List = ecron_tick:predict_datetime(activate, NewSpec, Start, End, 10, utc, Now), + List = ecron:predict_datetime(activate, NewSpec, Start, End, 10, utc, Now), ?WHENFAIL( io:format("Predict Failed: ~p ~p~n", [NewSpec, List]), prop_ecron:check_every_result(Second, diff --git a/test/prop_ecron_server.erl b/test/prop_ecron_server.erl index 2ada6ef..c87fd87 100644 --- a/test/prop_ecron_server.erl +++ b/test/prop_ecron_server.erl @@ -28,7 +28,7 @@ prop_server() -> application:set_env(ecron, local_jobs, []), application:set_env(ecron, global_jobs, []), application:ensure_all_started(ecron), - ecron_tick:clear(), + ecron:clear(), {History, State, Result} = run_commands(?MODULE, Cmds), ets:delete_all_objects(?LocalJob), ?WHENFAIL(io:format("History: ~p\nState: ~p\nResult: ~p\n", @@ -171,7 +171,7 @@ next_state(State, _Res, {call, _Mod, _Fun, _Args}) -> State. new_cron([Name, Spec, MFA]) -> new_cron([Name, Spec, MFA, {unlimited, unlimited}]); new_cron([Name, Spec, MFA, {Start, End}]) -> - {ok, Type, Crontab} = ecron:parse_spec(Spec), + {ok, Type, Crontab} = ecron_spec:parse_spec(Spec), #{type => Type, name => Name, crontab => Crontab, mfa => MFA, start_time => Start, end_time => End @@ -193,11 +193,11 @@ check_add_with_limit([Spec, _MFA, {StartTime, EndTime}], {error, already_ended}) is_expired(Spec, StartTime, EndTime). is_expired(Spec, StartTime, EndTime) -> - TZ = ecron_tick:get_time_zone(), - {ok, Type, Job} = ecron:parse_spec(Spec), - Start = ecron_tick:datetime_to_millisecond(TZ, StartTime), - End = ecron_tick:datetime_to_millisecond(TZ, EndTime), - [] =:= ecron_tick:predict_datetime(activate, #{type => Type, crontab => Job}, Start, End, 10, TZ, erlang:system_time(millisecond)). + TZ = ecron:get_time_zone(), + {ok, Type, Job} = ecron_spec:parse_spec(Spec), + Start = ecron:datetime_to_millisecond(TZ, StartTime), + End = ecron:datetime_to_millisecond(TZ, EndTime), + [] =:= ecron:predict_datetime(activate, #{type => Type, crontab => Job}, Start, End, 10, TZ, erlang:system_time(millisecond)). add_cron_new(Name, Spec, MFA) -> ecron:add(Name, Spec, MFA). add_cron_existing(Name, Spec, MFA) -> ecron:add(Name, Spec, MFA). diff --git a/test/prop_ecron_status.erl b/test/prop_ecron_status.erl index fdea8e0..3bd474f 100644 --- a/test/prop_ecron_status.erl +++ b/test/prop_ecron_status.erl @@ -171,7 +171,7 @@ prop_singleton() -> application:start(ecron), {ok, Name} = ecron:add(Name, "@every 1s", {timer, sleep, [1100]}, unlimited, unlimited, [{singleton, Singleton}]), timer:sleep(4200), - {ok, Res} = ecron_tick:statistic(Name), + {ok, Res} = ecron:statistic(Name), #{start_time := unlimited, end_time := unlimited, status := activate, failed := 0, ok := Ok, results := Results, run_microsecond := RunMs } = Res, @@ -215,7 +215,7 @@ prop_ecron_send_interval() -> Res1 = receive Message -> ok after 1100 -> error end, Res2 = receive Message -> ok after 1100 -> error end, Res3 = receive Message -> ok after 1100 -> error end, - {ok, Res} = ecron_tick:statistic(Job), + {ok, Res} = ecron:statistic(Job), #{start_time := unlimited, end_time := unlimited, status := activate, failed := 0, ok := Ok, results := Results, run_microsecond := RunMs } = Res, @@ -240,7 +240,7 @@ prop_auto_remove() -> EndTime = calendar:system_time_to_local_time(EndMs, millisecond), {ok, Name} = ecron:add_with_datetime(Name, "@every 1s", {timer, sleep, [500]}, unlimited, EndTime), timer:sleep(Shift + 100), - Result = ecron_tick:statistic(Name), + Result = ecron:statistic(Name), Result =:= {error, not_found} end). From 685234d66f51de87d2c69ae0441aa598953293f7 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 24 Nov 2020 16:19:18 +0800 Subject: [PATCH 03/11] update some monitor error --- src/ecron_app.erl | 1 + src/ecron_monitor.erl | 2 +- src/ecron_sup.erl | 2 +- test/prop_ecron_app_config.erl | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ecron_app.erl b/src/ecron_app.erl index af8d173..ada95a0 100644 --- a/src/ecron_app.erl +++ b/src/ecron_app.erl @@ -10,4 +10,5 @@ start(_StartType, _StartArgs) -> ecron_sup:start_link(). stop(_State) -> + rpc:abcast(nodes(), ?MONITOR_WORKER, {node(), ecron, stop}), ok. diff --git a/src/ecron_monitor.erl b/src/ecron_monitor.erl index 8ea2acc..18a15d1 100644 --- a/src/ecron_monitor.erl +++ b/src/ecron_monitor.erl @@ -39,7 +39,7 @@ handle_info(_Msg, State) -> {noreply, State}. health() -> - case erlang:whereis(?LocalJob) of + case erlang:whereis(?MONITOR_WORKER) of undefined -> {error, node()}; _ -> {ok, node()} end. diff --git a/src/ecron_sup.erl b/src/ecron_sup.erl index 3ebd9e0..184cf62 100644 --- a/src/ecron_sup.erl +++ b/src/ecron_sup.erl @@ -17,7 +17,7 @@ start_global(Measurements) -> case supervisor:start_child(?MODULE, #{ id => ?GLOBAL_WORKER, - start => {ecron_tick, start_link, [{global, ?GlobalJob}, GlobalJobs]}, + start => {ecron, start_link, [{global, ?GlobalJob}, GlobalJobs]}, restart => temporary, shutdown => 1000, type => worker, diff --git a/test/prop_ecron_app_config.erl b/test/prop_ecron_app_config.erl index fb16ac1..e3a6234 100644 --- a/test/prop_ecron_app_config.erl +++ b/test/prop_ecron_app_config.erl @@ -56,7 +56,7 @@ prop_application_error_config() -> Res = case T of {error, {{shutdown, - {failed_to_start_child, ecron_tick, + {failed_to_start_child, ecron, Reason}}, {ecron_app, start, [normal, []]}}} -> lists:member(Reason, [ {test, error_format}, From 7f295248ca6520f7127528c972b0aab3131c5362 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 24 Nov 2020 18:38:29 +0800 Subject: [PATCH 04/11] format by erlformat --- include/ecron.hrl | 16 +- rebar.config | 7 + src/ecron.app.src | 4 +- src/ecron.erl | 378 ++++++++++++++++++--------- src/ecron_app.erl | 1 + src/ecron_monitor.erl | 15 +- src/ecron_spec.erl | 234 ++++++++++++----- src/ecron_sup.erl | 50 ++-- test/prop_ecron_predict_datetime.erl | 5 +- 9 files changed, 484 insertions(+), 226 deletions(-) diff --git a/include/ecron.hrl b/include/ecron.hrl index f9e01d8..310ac12 100644 --- a/include/ecron.hrl +++ b/include/ecron.hrl @@ -3,7 +3,8 @@ -define(GlobalJob, ecron_global). -define(Ecron, ecron). --define(MAX_TIMEOUT, 4294967). %% (16#ffffffff div 1000) 49.71 days. +%% (16#ffffffff div 1000) 49.71 days. +-define(MAX_TIMEOUT, 4294967). -define(Success, [ecron, success]). -define(Failure, [ecron, failure]). -define(Activate, [ecron, activate]). @@ -12,5 +13,14 @@ -define(GlobalUp, [ecron, global, up]). -define(GlobalDown, [ecron, global, down]). --record(job, {name, status = activate, job, opts = [], ok = 0, failed = 0, - link = undefined, result = [], run_microsecond = []}). \ No newline at end of file +-record(job, { + name, + status = activate, + job, + opts = [], + ok = 0, + failed = 0, + link = undefined, + result = [], + run_microsecond = [] +}). diff --git a/rebar.config b/rebar.config index 76b7bf2..212dee6 100644 --- a/rebar.config +++ b/rebar.config @@ -17,3 +17,10 @@ {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, deprecated_function_calls, deprecated_functions]}. + +{project_plugins, [rebar3_format, erlfmt]}. +{format, [ + {files, ["src/*.erl", "include/*.hrl"]}, + {formatter, erlfmt_formatter}, %% The erlfmt formatter interface. + {options, #{print_width => 100, ignore_pragma => true}} %% ...or no options at all. +]}. diff --git a/src/ecron.app.src b/src/ecron.app.src index cd0b692..44505db 100644 --- a/src/ecron.app.src +++ b/src/ecron.app.src @@ -1,7 +1,7 @@ {application, ecron, [{description, "cron-like/crontab job scheduling library"}, - {vsn, "0.5.3"}, - {registered, [ecron_sup, ecron, ecron_monitor]}, + {vsn, "0.6.0"}, + {registered, [ecron_sup, ecron_local, ecron_monitor]}, {mod, {ecron_app, []}}, {applications, [kernel, stdlib, telemetry]}, {env, [ diff --git a/src/ecron.erl b/src/ecron.erl index a6ed3ad..65e1785 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -1,5 +1,7 @@ -module(ecron). + -include("ecron.hrl"). + -export([predict_datetime/2]). %% API Function @@ -19,12 +21,29 @@ -export([spawn_mfa/3, clear/0]). -record(state, {time_zone, max_timeout, timer_tab, job_tab}). --record(timer, {key, name, cur_count = 0, singleton, type, spec, mfa, link, - start_sec = unlimited, end_sec = unlimited, max_count = unlimited}). +-record(timer, { + key, + name, + cur_count = 0, + singleton, + type, + spec, + mfa, + link, + start_sec = unlimited, + end_sec = unlimited, + max_count = unlimited +}). -define(MAX_SIZE, 16). -define(SECONDS_FROM_0_TO_1970, 719528 * 86400). --define(day_of_week(Y, M, D), (case calendar:day_of_the_week(Y, M, D) of 7 -> 0; D1 -> D1 end)). +-define(day_of_week(Y, M, D), + (case calendar:day_of_the_week(Y, M, D) of + 7 -> 0; + D1 -> D1 + end) +). + -define(MatchNameSpec(Name), [{#timer{name = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Name}}]}]). %%%=================================================================== @@ -33,37 +52,45 @@ -type name() :: term(). -type crontab_spec() :: crontab() | string() | binary() | 1..4294967. --type crontab() :: #{second => '*' | [0..59 | {0..58, 1..59}, ...], -minute => '*' | [0..59 | {0..58, 1..59}, ...], -hour => '*' | [0..23, ...], -month => '*' | [1..12 | {1..11, 2..12}, ...], -day_of_month => '*' | [1..31 | {1..30, 2..31}, ...], -day_of_week => '*' | [0..6 | {0..5, 1..6}, ...]}. - --type mfargs() :: {M :: module(), F :: atom(), A :: [term()]}. --type ecron() :: #{name => name(), -crontab => crontab(), -start_time => calendar:rfc3339_string() | unlimited, -end_time => calendar:rfc3339_string() | unlimited, -mfa => mfargs(), -type => cron | every}. +-type crontab() :: #{ + second => '*' | [0..59 | {0..58, 1..59}, ...], + minute => '*' | [0..59 | {0..58, 1..59}, ...], + hour => '*' | [0..23, ...], + month => '*' | [1..12 | {1..11, 2..12}, ...], + day_of_month => '*' | [1..31 | {1..30, 2..31}, ...], + day_of_week => '*' | [0..6 | {0..5, 1..6}, ...] +}. + +-type mfargs() :: {M :: module(), F :: atom(), A :: [term()]}. +-type ecron() :: #{ + name => name(), + crontab => crontab(), + start_time => calendar:rfc3339_string() | unlimited, + end_time => calendar:rfc3339_string() | unlimited, + mfa => mfargs(), + type => cron | every +}. -type status() :: deactivate | activate. --type statistic() :: #{ecron => ecron(), -status => status(), -failed => non_neg_integer(), -ok => non_neg_integer(), -results => [term()], -run_microsecond => [pos_integer()], -time_zone => local | utc, -worker => pid(), -next => [calendar:datetime()]}. - --type parse_error() :: invalid_time | invalid_spec | month | day_of_month | day_of_week | hour | minute | second. +-type statistic() :: #{ + ecron => ecron(), + status => status(), + failed => non_neg_integer(), + ok => non_neg_integer(), + results => [term()], + run_microsecond => [pos_integer()], + time_zone => local | utc, + worker => pid(), + next => [calendar:datetime()] +}. + +-type parse_error() :: + invalid_time | invalid_spec | month | day_of_month | day_of_week | hour | minute | second. + -type start_datetime() :: unlimited | calendar:datetime(). -type end_datetime() :: unlimited | calendar:datetime(). --type option() :: {singleton, boolean()} |{max_count, pos_integer() | unlimited}. +-type option() :: {singleton, boolean()} | {max_count, pos_integer() | unlimited}. -type options() :: [option()]. %% @equiv add(JobName, Spec, MFA, unlimited, unlimited, []) @@ -112,20 +139,25 @@ add_with_datetime(JobName, Spec, MFA, Start, End) -> -spec add(name(), crontab_spec(), mfargs(), start_datetime(), end_datetime(), options()) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. add(JobName, Spec, MFA, Start, End, Opts) -> - case ecron_spec:is_start_end_ok(Start, End) of + case ecron_spec:is_start_end_datetime(Start, End) of true -> case ecron_spec:parse_spec(Spec) of {ok, Type, Crontab} -> Job = #{ - type => Type, name => JobName, - crontab => Crontab, mfa => MFA, - start_time => Start, end_time => End + type => Type, + name => JobName, + crontab => Crontab, + mfa => MFA, + start_time => Start, + end_time => End }, ValidOpts = ecron_spec:parse_valid_opts(Opts), gen_server:call(?LocalJob, {add, Job, ValidOpts}, infinity); - ErrParse -> ErrParse + ErrParse -> + ErrParse end; - false -> {error, invalid_time, {Start, End}} + false -> + {error, invalid_time, {Start, End}} end. %% @doc @@ -138,7 +170,7 @@ add(JobName, Spec, MFA, Start, End, Opts) -> %%
  • If Dest is a pid(), the timer will be automatically canceled if the process referred to by the pid() is not alive, or when the process exits.
  • %%
  • Warning: Cancels a timer by `erlang:cancel_timer(Ref)' not `ecron:delete/1'.
  • %% --spec send_after(crontab_spec(), pid()|atom(), term()) -> +-spec send_after(crontab_spec(), pid() | atom(), term()) -> {ok, reference()} | {error, parse_error(), term()}. send_after(Spec, Pid, Message) -> case parse_spec(Spec, 1) of @@ -146,12 +178,12 @@ send_after(Spec, Pid, Message) -> NextMs = calendar:rfc3339_to_system_time(Next, [{'unit', second}]) * 1000, Time = NextMs - erlang:system_time(millisecond), {ok, erlang:send_after(Time, Pid, Message)}; - Err -> Err + Err -> + Err end. %% @equiv send_interval(make_ref(), Spec, Pid, Message, unlimited, unlimited, []) --spec send_interval(crontab_spec(), pid(), term()) -> - {ok, name()} | {error, parse_error(), term()}. +-spec send_interval(crontab_spec(), pid(), term()) -> {ok, name()} | {error, parse_error(), term()}. send_interval(Spec, Pid, Message) -> send_interval(make_ref(), Spec, Pid, Message, unlimited, unlimited, []). @@ -176,9 +208,15 @@ send_interval(Spec, Message, Start, End, Option) -> %% %% %% --spec send_interval(name(), crontab_spec(), pid(), term(), start_datetime(), - end_datetime(), options()) -> - {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. +-spec send_interval( + name(), + crontab_spec(), + pid(), + term(), + start_datetime(), + end_datetime(), + options() +) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. send_interval(JobName, Spec, Pid, Message, Start, End, Option) -> add(JobName, Spec, {erlang, send, [Pid, Message]}, Start, End, Option). @@ -206,12 +244,14 @@ activate(JobName) -> gen_server:call(?LocalJob, {activate, JobName}, infinity). -spec statistic(name()) -> {ok, statistic()} | {error, not_found}. statistic(JobName) -> case ets:lookup(?LocalJob, JobName) of - [Job] -> {ok, job_to_statistic(Job)}; + [Job] -> + {ok, job_to_statistic(Job)}; [] -> try gen_server:call({global, ?GlobalJob}, {statistic, JobName}) - catch _:_ -> - {error, not_found} + catch + _:_ -> + {error, not_found} end end. @@ -223,8 +263,9 @@ statistic() -> Global = try gen_server:call({global, ?GlobalJob}, statistic) - catch _:_ -> - [] + catch + _:_ -> + [] end, Local ++ Global. @@ -242,13 +283,16 @@ reload() -> {error, atom(), term()}. parse_spec(Spec, Num) when is_integer(Num) andalso Num > 0 -> parse_spec_2(ecron_spec:parse_spec(Spec), Num). + parse_spec_2({ok, Type, JobSpec}, Num) -> Job = #{type => Type, crontab => JobSpec}, Next = predict_datetime(Job, Num), {ok, Job#{next => Next}}; -parse_spec_2({error, _Field, _Value} = Error, _Num) -> Error. +parse_spec_2({error, _Field, _Value} = Error, _Num) -> + Error. get_next_schedule_time(Name) -> gen_server:call(?LocalJob, {next_schedule_time, Name}, infinity). + clear() -> gen_server:call(?LocalJob, clear, infinity). predict_datetime(Job, Num) -> @@ -264,7 +308,8 @@ start_link({_, JobTab} = Name, JobSpec) -> {ok, Jobs} -> new_job_tab(JobTab), gen_server:start_link(Name, ?MODULE, [JobTab, Jobs], []); - {error, Reason} -> {error, Reason} + {error, Reason} -> + {error, Reason} end. init([JobTab, Jobs]) -> @@ -272,65 +317,73 @@ init([JobTab, Jobs]) -> TimerTab = ets:new(ecron_timer, [ordered_set, private, {keypos, #timer.key}]), TimeZone = get_time_zone(), MaxTimeout = application:get_env(?Ecron, adjusting_time_second, 7 * 24 * 3600) * 1000, - - [begin ets:insert_new(JobTab, Job) end || Job <- Jobs], - [begin add_job(JobTab, TimerTab, Job, TimeZone, Opts, true) - end || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(JobTab)], - - State = #state{timer_tab = TimerTab, job_tab = JobTab, max_timeout = MaxTimeout, time_zone = TimeZone}, + + [ + begin + ets:insert_new(JobTab, Job) + end + || Job <- Jobs + ], + [ + begin + add_job(JobTab, TimerTab, Job, TimeZone, Opts, true) + end + || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(JobTab) + ], + + State = #state{ + timer_tab = TimerTab, + job_tab = JobTab, + max_timeout = MaxTimeout, + time_zone = TimeZone + }, {ok, State, next_timeout(State)}. handle_call({add, Job, Opts}, _From, State) -> #state{timer_tab = TimerTab, job_tab = JobTab, time_zone = TimeZone} = State, Reply = add_job(JobTab, TimerTab, Job, TimeZone, Opts, false), {reply, Reply, State, tick(State)}; - handle_call({delete, Name}, _From, State) -> #state{timer_tab = TimerTab, job_tab = JobTab} = State, delete_job(JobTab, TimerTab, Name), {reply, ok, State, next_timeout(State)}; - handle_call({activate, Name}, _From, State) -> #state{job_tab = JobTab, time_zone = TimeZone, timer_tab = TimerTab} = State, Reply = activate_job(Name, JobTab, TimerTab, TimeZone), {reply, Reply, State, tick(State)}; - handle_call({deactivate, Name}, _From, State) -> #state{timer_tab = TimerTab, job_tab = JobTab} = State, Reply = deactivate_job(Name, JobTab, TimerTab), {reply, Reply, State, next_timeout(State)}; - handle_call({statistic, Name}, _From, State) -> Reply = job_to_statistic(Name, State), {reply, Reply, State, next_timeout(State)}; - handle_call(statistic, _From, State = #state{timer_tab = TimerTab}) -> Reply = - ets:foldl(fun(#timer{name = Name}, Acc) -> - {ok, Item} = job_to_statistic(Name, State), - [Item | Acc] - end, [], TimerTab), + ets:foldl( + fun(#timer{name = Name}, Acc) -> + {ok, Item} = job_to_statistic(Name, State), + [Item | Acc] + end, + [], + TimerTab + ), {reply, Reply, State, next_timeout(State)}; - handle_call({next_schedule_time, Name}, _From, State = #state{timer_tab = TimerTab}) -> Reply = get_next_schedule_time(TimerTab, Name), {reply, Reply, State, next_timeout(State)}; - handle_call(clear, _From, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> ets:delete_all_objects(TimerTab), ets:delete_all_objects(JobTab), {reply, ok, State, next_timeout(State)}; - handle_call(_Unknown, _From, State) -> {noreply, State, next_timeout(State)}. handle_info(timeout, State) -> {noreply, State, tick(State)}; - handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> delete_pid(Pid, TimerTab, JobTab), {noreply, State, next_timeout(State)}; - handle_info(_Unknown, State) -> {noreply, State, next_timeout(State)}. @@ -355,17 +408,24 @@ add_job(JobTab, TimerTab, Job, TimeZone, Opts, ForceUpdate) -> true -> Singleton = proplists:get_value(singleton, Opts), MaxCount = proplists:get_value(max_count, Opts), - InitTimer = #timer{singleton = Singleton, max_count = MaxCount, - name = Name, mfa = MFA, link = PidOrUndef}, + InitTimer = #timer{ + singleton = Singleton, + max_count = MaxCount, + name = Name, + mfa = MFA, + link = PidOrUndef + }, Now = current_millisecond(), telemetry:execute(?Activate, #{action_ms => Now}, #{name => Name, mfa => MFA}), update_timer(Now, InitTimer, Job, TimerTab, JobTab, TimeZone); - false -> {error, already_exist} + false -> + {error, already_exist} end. activate_job(Name, JobTab, TimerTab, TimeZone) -> case ets:lookup(JobTab, Name) of - [] -> {error, not_found}; + [] -> + {error, not_found}; [#job{job = Job, opts = Opts}] -> delete_job(JobTab, TimerTab, Name), case add_job(JobTab, TimerTab, Job, TimeZone, Opts, false) of @@ -380,13 +440,15 @@ deactivate_job(Name, JobTab, TimerTab) -> true -> telemetry:execute(?Deactivate, #{action_ms => current_millisecond()}, #{name => Name}), ok; - false -> {error, not_found} + false -> + {error, not_found} end. delete_job(JobTab, TimerTab, Name) -> ets:select_delete(TimerTab, ?MatchNameSpec(Name)), case ets:lookup(JobTab, Name) of - [] -> ok; + [] -> + ok; [#job{link = Link}] -> telemetry:execute(?Delete, #{action_ms => current_millisecond()}, #{name => Name}), unlink_pid(Link), @@ -397,13 +459,19 @@ delete_job(JobTab, TimerTab, Name) -> %%% Second Internal Functions %%%=================================================================== update_timer(Now, InitTimer, Job, TimeTab, JobTab, TimeZone) -> - #{name := Name, crontab := Spec, type := Type, start_time := StartTime, end_time := EndTime} = Job, + #{name := Name, crontab := Spec, type := Type, start_time := StartTime, end_time := EndTime} = + Job, Start = datetime_to_millisecond(TimeZone, StartTime), End = datetime_to_millisecond(TimeZone, EndTime), case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of {ok, NextSec} -> - Timer = InitTimer#timer{key = {NextSec, Name}, type = Type, - spec = Spec, start_sec = Start, end_sec = End}, + Timer = InitTimer#timer{ + key = {NextSec, Name}, + type = Type, + spec = Spec, + start_sec = Start, + end_sec = End + }, ets:insert(TimeTab, Timer), {ok, Name}; {error, already_ended} = Err -> @@ -420,25 +488,39 @@ next_schedule_millisecond(every, Sec, _TimeZone, Now, Start, End) -> end; next_schedule_millisecond(cron, Spec, TimeZone, Now, Start, End) -> ForwardDateTime = millisecond_to_datetime(TimeZone, Now + 1000), - DefaultMin = #{second => 0, minute => 0, hour => 0, - day_of_month => 1, month => 1, day_of_week => 0}, + DefaultMin = #{ + second => 0, + minute => 0, + hour => 0, + day_of_month => 1, + month => 1, + day_of_week => 0 + }, Min = spec_min(maps:to_list(Spec), DefaultMin), NextDateTime = next_schedule_datetime(Spec, Min, ForwardDateTime), Next = datetime_to_millisecond(TimeZone, NextDateTime), case in_range(Next, Start, End) of - {error, deactivate} -> next_schedule_millisecond(cron, Spec, TimeZone, Start - 1000, Start, End); - {error, already_ended} -> {error, already_ended}; - ok -> {ok, Next} + {error, deactivate} -> + next_schedule_millisecond(cron, Spec, TimeZone, Start - 1000, Start, End); + {error, already_ended} -> + {error, already_ended}; + ok -> + {ok, Next} end. next_schedule_datetime(DateSpec, Min, DateTime) -> #{ - second := SecondSpec, minute := MinuteSpec, hour := HourSpec, - day_of_month := DayOfMonthSpec, month := MonthSpec, - day_of_week := DayOfWeekSpec} = DateSpec, + second := SecondSpec, + minute := MinuteSpec, + hour := HourSpec, + day_of_month := DayOfMonthSpec, + month := MonthSpec, + day_of_week := DayOfWeekSpec + } = DateSpec, {{Year, Month, Day}, {Hour, Minute, Second}} = DateTime, case valid_datetime(MonthSpec, Month) of - false -> forward_month(DateTime, Min, DateSpec); + false -> + forward_month(DateTime, Min, DateSpec); true -> case valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) of false -> @@ -446,10 +528,12 @@ next_schedule_datetime(DateSpec, Min, DateTime) -> forward_day(DateTime, Min, LastDay, DateSpec); true -> case valid_datetime(HourSpec, Hour) of - false -> forward_hour(DateTime, Min, DateSpec); + false -> + forward_hour(DateTime, Min, DateSpec); true -> case valid_datetime(MinuteSpec, Minute) of - false -> forward_minute(DateTime, Min, DateSpec); + false -> + forward_minute(DateTime, Min, DateSpec); true -> case valid_datetime(SecondSpec, Second) of false -> forward_second(DateTime, Min, DateSpec); @@ -465,7 +549,8 @@ tick(State = #state{timer_tab = TimerTab}) -> current_millisecond() -> erlang:system_time(millisecond). -tick_tick('$end_of_table', _Cur, _State) -> infinity; +tick_tick('$end_of_table', _Cur, _State) -> + infinity; tick_tick({Due, _Name}, Cur, #state{max_timeout = MaxTimeout}) when Due > Cur -> min(Due - Cur, MaxTimeout); tick_tick(Key = {Due, Name}, Cur, State) -> @@ -489,9 +574,11 @@ maybe_spawn_worker(true, Pid, Name, MFA, JobTab) when is_pid(Pid) -> true -> {0, Pid}; false -> {1, spawn(?MODULE, spawn_mfa, [JobTab, Name, MFA])} end; -maybe_spawn_worker(false, Singleton, _Name, _MFA, _JobTab) -> {0, Singleton}. +maybe_spawn_worker(false, Singleton, _Name, _MFA, _JobTab) -> + {0, Singleton}. -update_next_schedule(Max, Max, _Cron, _Cur, Name, _TZ, _CurPid, Tab, JobTab) -> delete_job(JobTab, Tab, Name); +update_next_schedule(Max, Max, _Cron, _Cur, Name, _TZ, _CurPid, Tab, JobTab) -> + delete_job(JobTab, Tab, Name); update_next_schedule(Count, _Max, Cron, Cur, Name, TZ, CurPid, Tab, JobTab) -> #timer{type = Type, start_sec = Start, end_sec = End, spec = Spec} = Cron, case next_schedule_millisecond(Type, Spec, TZ, Cur, Start, End) of @@ -510,8 +597,10 @@ spawn_mfa(JobTab, Name, MFA) -> {erlang, send, [Pid, Message]} -> erlang:send(Pid, Message), {?Success, 1, 0, Message}; - {M, F, A} -> {?Success, 1, 0, apply(M, F, A)}; - {F, A} -> {?Success, 1, 0, apply(F, A)} + {M, F, A} -> + {?Success, 1, 0, apply(M, F, A)}; + {F, A} -> + {?Success, 1, 0, apply(F, A)} end catch Error:Reason:Stacktrace -> @@ -519,14 +608,21 @@ spawn_mfa(JobTab, Name, MFA) -> end, End = erlang:monotonic_time(), Cost = erlang:convert_time_unit(End - Start, native, microsecond), - telemetry:execute(Event, #{run_microsecond => Cost, run_result => NewRes}, #{name => Name, mfa => MFA}), + telemetry:execute(Event, #{run_microsecond => Cost, run_result => NewRes}, #{ + name => Name, + mfa => MFA + }), case ets:lookup(JobTab, Name) of - [] -> ok; + [] -> + ok; [Job] -> #job{ok = Ok, failed = Failed, run_microsecond = RunMs, result = Results} = Job, - Elements = [{#job.ok, Ok + OkInc}, {#job.failed, Failed + FailedInc}, + Elements = [ + {#job.ok, Ok + OkInc}, + {#job.failed, Failed + FailedInc}, {#job.run_microsecond, lists:sublist([Cost | RunMs], ?MAX_SIZE)}, - {#job.result, lists:sublist([NewRes | Results], ?MAX_SIZE)}], + {#job.result, lists:sublist([NewRes | Results], ?MAX_SIZE)} + ], ets:update_element(JobTab, Name, Elements) end. @@ -542,7 +638,8 @@ forward_minute(DateTime, Min, Spec) -> {{Year, Month, Day}, {Hour, Minute, _Second}} = DateTime, NewMinute = nearest(minute, Minute, 59, Spec), case Minute >= NewMinute of - true -> forward_hour(DateTime, Min, Spec); + true -> + forward_hour(DateTime, Min, Spec); false -> #{second := SecondM} = Min, {{Year, Month, Day}, {Hour, NewMinute, SecondM}} @@ -563,7 +660,8 @@ forward_hour(DateTime, Min, Spec) -> forward_day(DateTime, Min, LastDay, Spec) -> {{Year, Month, Day}, {_Hour, _Minute, _Second}} = DateTime, case Day + 1 of - NewDay when NewDay > LastDay -> forward_month(DateTime, Min, Spec); + NewDay when NewDay > LastDay -> + forward_month(DateTime, Min, Spec); NewDay -> #{hour := HourM, minute := MinuteM, second := SecondM} = Min, NewDateTime = {{Year, Month, NewDay}, {HourM, MinuteM, SecondM}}, @@ -580,19 +678,21 @@ forward_month(DateTime, Min, Spec) -> #{month := MonthM, hour := HourM, minute := MinuteM, second := SecondM} = Min, NewDateTime = {{NYear, NMonth, NDay}, {_NHour, _NMinute, _NSecond}} = - case Month >= NewMonth of - true -> {{Year + 1, MonthM, 1}, {HourM, MinuteM, SecondM}}; - false -> {{Year, NewMonth, 1}, {HourM, MinuteM, SecondM}} - end, + case Month >= NewMonth of + true -> {{Year + 1, MonthM, 1}, {HourM, MinuteM, SecondM}}; + false -> {{Year, NewMonth, 1}, {HourM, MinuteM, SecondM}} + end, #{day_of_week := DayOfWeekSpec, day_of_month := DayOfMonthSpec} = Spec, case valid_day(NYear, NMonth, NDay, DayOfMonthSpec, DayOfWeekSpec) of false -> LastDay = calendar:last_day_of_the_month(NYear, NMonth), forward_day(NewDateTime, Min, LastDay, Spec); - true -> NewDateTime + true -> + NewDateTime end. -datetime_to_millisecond(_, unlimited) -> unlimited; +datetime_to_millisecond(_, unlimited) -> + unlimited; datetime_to_millisecond(local, DateTime) -> UtcTime = erlang:localtime_to_universaltime(DateTime), datetime_to_millisecond(utc, UtcTime); @@ -606,10 +706,14 @@ nearest(Type, Current, Max, Spec) -> Values = maps:get(Type, Spec), nearest_1(Values, Values, Max, Current + 1). -nearest_1('*', '*', MaxLimit, Next) when Next > MaxLimit -> 1; -nearest_1('*', '*', _MaxLimit, Next) -> Next; -nearest_1([], [{Min, _} | _], _Max, _Next) -> Min; -nearest_1([], [Min | _], _Max, _Next) -> Min; +nearest_1('*', '*', MaxLimit, Next) when Next > MaxLimit -> + 1; +nearest_1('*', '*', _MaxLimit, Next) -> + Next; +nearest_1([], [{Min, _} | _], _Max, _Next) -> + Min; +nearest_1([], [Min | _], _Max, _Next) -> + Min; nearest_1([{Min, Max} | Rest], Spec, MaxLimit, Next) -> if Next > Max -> nearest_1(Rest, Spec, MaxLimit, Next); @@ -628,7 +732,8 @@ valid_datetime([Value | _T], Value) -> true; valid_datetime([{Lower, Upper} | _], Value) when Lower =< Value andalso Value =< Upper -> true; valid_datetime([_ | T], Value) -> valid_datetime(T, Value). -valid_day(_Year, _Month, _Day, '*', '*') -> true; +valid_day(_Year, _Month, _Day, '*', '*') -> + true; valid_day(_Year, _Month, Day, DayOfMonthSpec, '*') -> valid_datetime(DayOfMonthSpec, Day); valid_day(Year, Month, Day, '*', DayOfWeekSpec) -> @@ -639,10 +744,12 @@ valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) -> false -> DayOfWeek = ?day_of_week(Year, Month, Day), valid_datetime(DayOfWeekSpec, DayOfWeek); - true -> true + true -> + true end. -spec_min([], Acc) -> Acc; +spec_min([], Acc) -> + Acc; spec_min([{Key, Value} | Rest], Acc) -> NewAcc = case Value of @@ -670,21 +777,28 @@ in_range(_Current, _Start, _End) -> ok. to_rfc3339(unlimited) -> unlimited; to_rfc3339(Next) -> calendar:system_time_to_rfc3339(Next div 1000, [{unit, second}]). -predict_datetime(deactivate, _, _, _, _, _, _) -> []; +predict_datetime(deactivate, _, _, _, _, _, _) -> + []; predict_datetime(activate, #{type := every, crontab := Sec} = Job, Start, End, Num, TimeZone, NowT) -> - Now = case maps:find(name, Job) of error -> NowT; _ -> NowT - Sec * 1000 end, + Now = + case maps:find(name, Job) of + error -> NowT; + _ -> NowT - Sec * 1000 + end, predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []); predict_datetime(activate, Job, Start, End, Num, TimeZone, Now) -> predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []). -predict_datetime_2(_Job, _TimeZone, _Now, _Start, _End, 0, Acc) -> lists:reverse(Acc); +predict_datetime_2(_Job, _TimeZone, _Now, _Start, _End, 0, Acc) -> + lists:reverse(Acc); predict_datetime_2(Job, TimeZone, Now, Start, End, Num, Acc) -> #{type := Type, crontab := Spec} = Job, case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of {ok, Next} -> NewAcc = [to_rfc3339(Next) | Acc], predict_datetime_2(Job, TimeZone, Next, Start, End, Num - 1, NewAcc); - {error, already_ended} -> lists:reverse(Acc) + {error, already_ended} -> + lists:reverse(Acc) end. get_next_schedule_time(Timer, Name) -> @@ -707,7 +821,8 @@ link_send_pid({erlang, send, [PidOrName, _Message]}) -> Pid = get_pid(PidOrName), is_pid(Pid) andalso (catch link(Pid)), Pid; -link_send_pid(_MFA) -> undefined. +link_send_pid(_MFA) -> + undefined. unlink_pid(Pid) when is_pid(Pid) -> catch unlink(Pid); unlink_pid(_) -> ok. @@ -718,7 +833,7 @@ get_pid(Name) when is_atom(Name) -> whereis(Name). job_to_statistic(Job = #job{name = Name}) -> TZ = get_time_zone(), Next = get_next_schedule_time(Name), - job_to_statistic(Job, TZ, Next). + job_to_statistic(Job, TZ, Next - 1000). job_to_statistic(Name, State) -> #state{timer_tab = Timer, job_tab = JobTab, time_zone = TZ} = State, @@ -726,20 +841,35 @@ job_to_statistic(Name, State) -> [Job] -> Next = get_next_schedule_time(Timer, Name), {ok, job_to_statistic(Job, TZ, Next)}; - [] -> {error, not_found} + [] -> + {error, not_found} end. job_to_statistic(Job, TimeZone, Now) -> - #job{job = JobSpec, status = Status, opts = Opts, - ok = Ok, failed = Failed, result = Res, run_microsecond = RunMs} = Job, + #job{ + job = JobSpec, + status = Status, + opts = Opts, + ok = Ok, + failed = Failed, + result = Res, + run_microsecond = RunMs + } = Job, #{start_time := StartTime, end_time := EndTime} = JobSpec, Start = datetime_to_millisecond(TimeZone, StartTime), End = datetime_to_millisecond(TimeZone, EndTime), - JobSpec#{status => Status, ok => Ok, failed => Failed, opts => Opts, + JobSpec#{ + status => Status, + ok => Ok, + failed => Failed, + opts => Opts, next => predict_datetime(Status, JobSpec, Start, End, ?MAX_SIZE, TimeZone, Now), start_time => to_rfc3339(datetime_to_millisecond(TimeZone, StartTime)), end_time => to_rfc3339(datetime_to_millisecond(TimeZone, EndTime)), - node => node(), results => Res, run_microsecond => RunMs}. + node => node(), + results => Res, + run_microsecond => RunMs + }. %% For PropEr Test -ifdef(TEST). diff --git a/src/ecron_app.erl b/src/ecron_app.erl index ada95a0..559c980 100644 --- a/src/ecron_app.erl +++ b/src/ecron_app.erl @@ -2,6 +2,7 @@ -module(ecron_app). -behaviour(application). + -include_lib("ecron.hrl"). -export([start/2, stop/1]). diff --git a/src/ecron_monitor.erl b/src/ecron_monitor.erl index 18a15d1..9e111e8 100644 --- a/src/ecron_monitor.erl +++ b/src/ecron_monitor.erl @@ -1,14 +1,16 @@ %%% @private -module(ecron_monitor). + -behaviour(gen_server). -export([start_link/2]). -export([health/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). + -include("ecron.hrl"). start_link(Name, Jobs) -> - case ecron_tick:parse_crontab(Jobs, []) of + case ecron_spec:parse_crontab(Jobs, []) of {ok, [_ | _]} -> gen_server:start_link(Name, ?MODULE, [], []); {error, Reason} -> {error, Reason} end. @@ -28,13 +30,18 @@ handle_info(_Msg, State) -> QuorumSize = application:get_env(ecron, global_quorum_size, 1), {ResL, Bad} = rpc:multicall([node() | nodes(visible)], ?MODULE, health, [], 5000), {Healthy, GoodNodes, BadNodes} = split(ResL, 0, [], Bad), - Measurements = #{action_ms => erlang:system_time(millisecond), - quorum_size => QuorumSize, good_nodes => GoodNodes, bad_nodes => BadNodes}, + Measurements = #{ + action_ms => erlang:system_time(millisecond), + quorum_size => QuorumSize, + good_nodes => GoodNodes, + bad_nodes => BadNodes + }, case Healthy >= QuorumSize of true -> {ok, Pid} = ecron_sup:start_global(Measurements), link(Pid); - false -> ecron_sup:stop_global(Measurements) + false -> + ecron_sup:stop_global(Measurements) end, {noreply, State}. diff --git a/src/ecron_spec.erl b/src/ecron_spec.erl index 67b79bc..dec2841 100644 --- a/src/ecron_spec.erl +++ b/src/ecron_spec.erl @@ -1,23 +1,41 @@ %%% @private -module(ecron_spec). + -include("ecron.hrl"). %% API -export([parse_spec/1]). -export([parse_crontab/2]). --export([is_start_end_ok/2]). +-export([is_start_end_datetime/2]). -export([parse_valid_opts/1]). %% @private -parse_spec("@yearly") -> parse_spec("0 0 1 1 *"); % Run once a year, midnight, Jan. 1st -parse_spec("@annually") -> parse_spec("0 0 1 1 *"); % Same as @yearly -parse_spec("@monthly") -> parse_spec("0 0 1 * *"); % Run once a month, midnight, first of month -parse_spec("@weekly") -> parse_spec("0 0 * * 0"); % Run once a week, midnight between Sat/Sun -parse_spec("@midnight") -> parse_spec("0 0 * * *"); % Run once a day, midnight -parse_spec("@daily") -> parse_spec("0 0 * * *"); % Same as @midnight -parse_spec("@hourly") -> parse_spec("0 * * * *"); % Run once an hour, beginning of hour -parse_spec("@minutely") -> parse_spec("* * * * *"); -parse_spec(Bin) when is_binary(Bin) -> parse_spec(binary_to_list(Bin)); + +% Run once a year, midnight, Jan. 1st +parse_spec("@yearly") -> + parse_spec("0 0 0 1 1 *"); +% Same as @yearly +parse_spec("@annually") -> + parse_spec("0 0 0 1 1 *"); +% Run once a month, midnight, first of month +parse_spec("@monthly") -> + parse_spec("0 0 0 1 * *"); +% Run once a week, midnight between Sat/Sun +parse_spec("@weekly") -> + parse_spec("0 0 0 * * 0"); +% Run once a day, midnight +parse_spec("@midnight") -> + parse_spec("0 0 0 * * *"); +% Same as @midnight +parse_spec("@daily") -> + parse_spec("0 0 0 * * *"); +% Run once an hour, beginning of hour +parse_spec("@hourly") -> + parse_spec("0 0 * * * *"); +parse_spec("@minutely") -> + parse_spec("0 * * * * *"); +parse_spec(Bin) when is_binary(Bin) -> + parse_spec(binary_to_list(Bin)); parse_spec(List) when is_list(List) -> case string:tokens(string:lowercase(List), " ") of [_S, _M, _H, _DOM, _Mo, _DOW] = Cron -> parse_cron_spec(Cron); @@ -29,15 +47,21 @@ parse_spec(Spec) when is_map(Spec) -> {Months, NewSpec} = take(month, Spec), case unzip(Months, 1, 12, []) of {ok, EMonths} -> - List = [{second, 0, 59}, {minute, 0, 59}, {hour, 0, 23}, + List = [ + {second, 0, 59}, + {minute, 0, 59}, + {hour, 0, 23}, {day_of_month, 1, get_max_day_of_months(EMonths)}, - {day_of_week, 0, 6}], + {day_of_week, 0, 6} + ], format_map_spec(List, NewSpec, #{month => zip(EMonths)}); - error -> {error, month, Months} + error -> + {error, month, Months} end; parse_spec(Second) when is_integer(Second) andalso Second =< ?MAX_TIMEOUT -> {ok, every, Second}; -parse_spec(Spec) -> {error, invalid_spec, Spec}. +parse_spec(Spec) -> + {error, invalid_spec, Spec}. parse_cron_spec([Second, Minute, Hour, DayOfMonth, Month, DayOfWeek]) -> case parse_field(Month, 1, 12) of @@ -47,59 +71,72 @@ parse_cron_spec([Second, Minute, Hour, DayOfMonth, Month, DayOfWeek]) -> {minute, Minute, 0, 59}, {hour, Hour, 0, 23}, {day_of_month, DayOfMonth, 1, get_max_day_of_months(Months)}, - {day_of_week, DayOfWeek, 0, 6}], + {day_of_week, DayOfWeek, 0, 6} + ], parse_fields(Fields, #{month => Months}); - error -> {error, month, Month} + error -> + {error, month, Month} end. -parse_fields([], Acc) -> {ok, cron, Acc}; +parse_fields([], Acc) -> + {ok, cron, Acc}; parse_fields([{Key, Spec, Min, Max} | Rest], Acc) -> case parse_field(Spec, Min, Max) of {ok, V} -> parse_fields(Rest, Acc#{Key => V}); error -> {error, Key, Spec} end. -parse_field("*", _Min, _Max) -> {ok, '*'}; +parse_field("*", _Min, _Max) -> + {ok, '*'}; parse_field(Value, MinLimit, MaxLimit) -> parse_field(string:tokens(Value, ","), MinLimit, MaxLimit, []). -parse_field([], _MinLimit, _MaxLimit, Acc) -> {ok, zip(lists:usort(Acc))}; +parse_field([], _MinLimit, _MaxLimit, Acc) -> + {ok, zip(lists:usort(Acc))}; parse_field([Field | Fields], MinL, MaxL, Acc) -> case string:tokens(Field, "-") of [Field] -> case string:tokens(Field, "/") of - [_] -> % Integer + % Integer + [_] -> Int = field_to_int(Field), case Int >= MinL andalso Int =< MaxL of true -> parse_field(Fields, MinL, MaxL, [Int | Acc]); false -> error end; - ["*", StepStr] -> % */Step -> MinLimit~MaxLimit/Step + % */Step -> MinLimit~MaxLimit/Step + ["*", StepStr] -> case field_to_int(StepStr) of Step when Step > 0 -> NewAcc = lists:seq(MinL, MaxL, Step) ++ Acc, parse_field(Fields, MinL, MaxL, NewAcc); - _ -> error + _ -> + error end; - [MinStr, StepStr] -> % Min/Step -> Min~MaxLimit/Step + % Min/Step -> Min~MaxLimit/Step + [MinStr, StepStr] -> Min = field_to_int(MinStr), Step = field_to_int(StepStr), case Min >= MinL andalso Min =< MaxL andalso Step > 0 of true -> NewAcc = lists:seq(Min, MaxL, Step) ++ Acc, parse_field(Fields, MinL, MaxL, NewAcc); - false -> error + false -> + error end; - _ -> error + _ -> + error end; [MinStr, MaxStepStr] -> case field_to_int(MinStr) of - Min when Min >= MinL andalso Min =< MaxL -> % Min-Max/Step -> Min~Max/Step + % Min-Max/Step -> Min~Max/Step + Min when Min >= MinL andalso Min =< MaxL -> {Max, Step} = case string:tokens(MaxStepStr, "/") of [_] -> {field_to_int(MaxStepStr), 1}; [MaxStr, StepStr] -> {field_to_int(MaxStr), field_to_int(StepStr)}; - _ -> {-1, -1} %% error + %% error + _ -> {-1, -1} end, case Max >= MinL andalso Max >= Min andalso Step > 0 of true -> @@ -108,27 +145,38 @@ parse_field([Field | Fields], MinL, MaxL, Acc) -> true -> parse_field(Fields, MinL, MaxL, New ++ Acc); false -> error end; - false -> error + false -> + error end; - _ -> error + _ -> + error end; - _ -> error + _ -> + error end. zip('*') -> '*'; zip([T | Rest]) -> zip(Rest, T + 1, [T], []). -zip([], _, [Single], Acc) -> lists:reverse([Single | Acc]); -zip([], _, [One, Two], Acc) -> lists:reverse([One, Two | Acc]); -zip([], _, Buffer, Acc) -> lists:reverse([{lists:min(Buffer), lists:max(Buffer)} | Acc]); -zip([L | Rest], L, Buffer, Acc) -> zip(Rest, L + 1, [L | Buffer], Acc); -zip([F | Rest], _Last, [Single], Acc) -> zip(Rest, F + 1, [F], [Single | Acc]); -zip([F | Rest], _Last, [One, Two], Acc) -> zip(Rest, F + 1, [F], [One, Two | Acc]); +zip([], _, [Single], Acc) -> + lists:reverse([Single | Acc]); +zip([], _, [One, Two], Acc) -> + lists:reverse([One, Two | Acc]); +zip([], _, Buffer, Acc) -> + lists:reverse([{lists:min(Buffer), lists:max(Buffer)} | Acc]); +zip([L | Rest], L, Buffer, Acc) -> + zip(Rest, L + 1, [L | Buffer], Acc); +zip([F | Rest], _Last, [Single], Acc) -> + zip(Rest, F + 1, [F], [Single | Acc]); +zip([F | Rest], _Last, [One, Two], Acc) -> + zip(Rest, F + 1, [F], [One, Two | Acc]); zip([F | Rest], _Last, Buffer, Acc) -> zip(Rest, F + 1, [F], [{lists:min(Buffer), lists:max(Buffer)} | Acc]). -unzip('*', _MinLimit, _MaxLimit, _Acc) -> {ok, '*'}; -unzip([], _MinLimit, _MaxLimit, Acc) -> {ok, lists:usort(Acc)}; +unzip('*', _MinLimit, _MaxLimit, _Acc) -> + {ok, '*'}; +unzip([], _MinLimit, _MaxLimit, Acc) -> + {ok, lists:usort(Acc)}; unzip([{Min, Max} | List], MinL, MaxL, Acc) -> NewMin = field_to_int(Min), NewMax = field_to_int(Max), @@ -142,7 +190,6 @@ unzip([Int | List], MinL, MaxL, Acc) -> _ -> error end. - parse_every_spec(SecSpec) -> LowerSecSpec = string:lowercase(SecSpec), List = [{"d", 24 * 3600}, {"h", 3600}, {"m", 60}, {"s", 1}], @@ -152,17 +199,20 @@ parse_every_spec(SecSpec) -> error -> {error, second, SecSpec} end. -parse_every(_, "", Sum) -> {ok, Sum}; -parse_every([], _, _Sum) -> error; +parse_every(_, "", Sum) -> + {ok, Sum}; +parse_every([], _, _Sum) -> + error; parse_every([{Sep, Index} | Rest], Spec, Sum) -> case parse_every(Spec, Sep) of {Val, NewSpec} -> parse_every(Rest, NewSpec, Val * Index + Sum); error -> error end. -parse_every(Spec, Seps) -> - case string:tokens(Spec, Seps) of - [Spec] -> {0, Spec}; +parse_every(Spec, Sep) -> + case string:tokens(Spec, Sep) of + [Spec] -> + {0, Spec}; [Str, S] -> case field_to_int(Str) of Value when Value >= 0 -> {Value, S}; @@ -173,15 +223,20 @@ parse_every(Spec, Seps) -> Value when Value >= 0 -> {Value, ""}; _ -> error end; - _ -> error + _ -> + error end. get_max_day_of_months('*') -> 31; get_max_day_of_months(List) -> max_day_of_months(List, 29). -max_day_of_months([], Max) -> Max; -max_day_of_months(_, 31) -> 31; -max_day_of_months([{_Min, _Max} | _List], _OldMax) -> 31; %% because Max - Min >= 2 +max_day_of_months([], Max) -> + Max; +max_day_of_months(_, 31) -> + 31; +%% because Max - Min >= 2 +max_day_of_months([{_Min, _Max} | _List], _OldMax) -> + 31; max_day_of_months([Int | List], Max) -> NewMax = erlang:max(Max, last_day_of_month(Int)), max_day_of_months(List, NewMax). @@ -194,23 +249,45 @@ last_day_of_month(11) -> 30; last_day_of_month(M) when is_integer(M), M > 0, M < 13 -> 31. -define(Alphabet, #{ - "sun" => 0, "mon" => 1, "tue" => 2, "wed" => 3, "thu" => 4, "fir" => 5, "sat" => 6, - "jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6, - "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12}). + "sun" => 0, + "mon" => 1, + "tue" => 2, + "wed" => 3, + "thu" => 4, + "fir" => 5, + "sat" => 6, + "jan" => 1, + "feb" => 2, + "mar" => 3, + "apr" => 4, + "may" => 5, + "jun" => 6, + "jul" => 7, + "aug" => 8, + "sep" => 9, + "oct" => 10, + "nov" => 11, + "dec" => 12 +}). -field_to_int(Int) when is_integer(Int) -> Int; +field_to_int(Int) when is_integer(Int) -> + Int; field_to_int(List) when is_list(List) -> case maps:find(List, ?Alphabet) of error -> case string:list_to_integer(List) of {Int, []} -> Int; - _ -> -1 %% error + %% error + _ -> -1 end; - {ok, Int} -> Int + {ok, Int} -> + Int end. -format_map_spec([], Old, New) when Old =:= #{} -> {ok, cron, New}; -format_map_spec([], Old, _New) -> {error, maps:keys(Old), maps:values(Old)}; +format_map_spec([], Old, New) when Old =:= #{} -> + {ok, cron, New}; +format_map_spec([], Old, _New) -> + {error, maps:keys(Old), maps:values(Old)}; format_map_spec([{Key, Min, Max} | List], Old, New) -> {Value, Old1} = take(Key, Old), case unzip(Value, Min, Max, []) of @@ -225,7 +302,8 @@ take(Key, Spec) -> Res -> Res end. -parse_crontab([], Acc) -> {ok, Acc}; +parse_crontab([], Acc) -> + {ok, Acc}; parse_crontab([{Name, Spec, {_M, _F, _A} = MFA} | Jobs], Acc) -> parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, unlimited, unlimited, []} | Jobs], Acc); parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End} | Jobs], Acc) -> @@ -237,21 +315,33 @@ parse_crontab([{Name, Spec, {_M, _F, _A} = MFA, Start, End, Opts} | Jobs], Acc) false -> parse_crontab(Jobs, [Job | Acc]); _ -> {error, lists:flatten(io_lib:format("Duplicate job name: ~p", [Name]))} end; - {error, Field, Reason} -> {error, lists:flatten(io_lib:format("~p: ~p", [Field, Reason]))} + {error, Field, Reason} -> + {error, lists:flatten(io_lib:format("~p: ~p", [Field, Reason]))} end; -parse_crontab([L | _], _Acc) -> {error, L}. +parse_crontab([L | _], _Acc) -> + {error, L}. parse_job(JobName, Spec, MFA, Start, End, Opts) -> - case is_start_end_ok(Start, End) of + case is_start_end_datetime(Start, End) of true -> case parse_spec(Spec) of {ok, Type, Crontab} -> - Job = #{type => Type, name => JobName, crontab => Crontab, mfa => MFA, - start_time => Start, end_time => End}, - {ok, #job{name = JobName, - status = activate, job = Job, - opts = parse_valid_opts(Opts)}}; - ErrParse -> ErrParse + Job = #{ + type => Type, + name => JobName, + crontab => Crontab, + mfa => MFA, + start_time => Start, + end_time => End + }, + {ok, #job{ + name = JobName, + status = activate, + job = Job, + opts = parse_valid_opts(Opts) + }}; + ErrParse -> + ErrParse end; false -> {error, invalid_time, {Start, End}} @@ -262,23 +352,25 @@ parse_valid_opts(Opts) -> MaxCount = proplists:get_value(max_count, Opts, unlimited), [{singleton, Singleton}, {max_count, MaxCount}]. -%% @private -is_start_end_ok(Start, End) -> +is_start_end_datetime(Start, End) -> case is_datetime(Start) andalso is_datetime(End) of true when Start =/= unlimited andalso End =/= unlimited -> EndSec = calendar:datetime_to_gregorian_seconds(End), StartSec = calendar:datetime_to_gregorian_seconds(Start), EndSec > StartSec; - Res -> Res + Res -> + Res end. -is_datetime(unlimited) -> true; +is_datetime(unlimited) -> + true; is_datetime({Date, {H, M, S}}) -> (is_integer(H) andalso H >= 0 andalso H =< 23) andalso (is_integer(M) andalso M >= 0 andalso M =< 59) andalso (is_integer(S) andalso S >= 0 andalso H =< 59) andalso calendar:valid_date(Date); -is_datetime(_ErrFormat) -> false. +is_datetime(_ErrFormat) -> + false. %% For PropEr Test -ifdef(TEST). diff --git a/src/ecron_sup.erl b/src/ecron_sup.erl index 184cf62..4811d8f 100644 --- a/src/ecron_sup.erl +++ b/src/ecron_sup.erl @@ -1,6 +1,8 @@ %%% @private -module(ecron_sup). + -behaviour(supervisor). + -include("ecron.hrl"). -export([start_global/1, stop_global/1]). @@ -14,20 +16,26 @@ start_link() -> start_global(Measurements) -> GlobalJobs = application:get_env(?Ecron, global_jobs, []), - case supervisor:start_child(?MODULE, - #{ - id => ?GLOBAL_WORKER, - start => {ecron, start_link, [{global, ?GlobalJob}, GlobalJobs]}, - restart => temporary, - shutdown => 1000, - type => worker, - modules => [?GLOBAL_WORKER] - }) of + case + supervisor:start_child( + ?MODULE, + #{ + id => ?GLOBAL_WORKER, + start => {ecron, start_link, [{global, ?GlobalJob}, GlobalJobs]}, + restart => temporary, + shutdown => 1000, + type => worker, + modules => [?GLOBAL_WORKER] + } + ) + of {ok, Pid} -> telemetry:execute(?GlobalUp, Measurements, #{self => node()}), {ok, Pid}; - {error, {already_started, Pid}} -> {ok, Pid}; - {error, {{already_started, Pid}, _}} -> {ok, Pid} + {error, {already_started, Pid}} -> + {ok, Pid}; + {error, {{already_started, Pid}, _}} -> + {ok, Pid} end. stop_global(Measurements) -> @@ -53,16 +61,16 @@ init([]) -> modules => [?LOCAL_WORKER] }, case GlobalJobs of - [] -> {ok, {SupFlags, [Local]}}; + [] -> + {ok, {SupFlags, [Local]}}; _ -> - Global = - #{ - id => ?MONITOR_WORKER, - start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [?MONITOR_WORKER] - }, + Global = #{ + id => ?MONITOR_WORKER, + start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [?MONITOR_WORKER] + }, {ok, {SupFlags, [Local, Global]}} end. diff --git a/test/prop_ecron_predict_datetime.erl b/test/prop_ecron_predict_datetime.erl index 5ebfc7d..6b9a198 100644 --- a/test/prop_ecron_predict_datetime.erl +++ b/test/prop_ecron_predict_datetime.erl @@ -75,7 +75,10 @@ predict_cron_datetime(Start, End, Job, {TimeZone, Now}, Num, Acc) -> Next = next_schedule_datetime(Job, Now), case in_range(Next, Start, End) of already_ended -> lists:reverse(Acc); - deactivate -> predict_cron_datetime(Start, End, Job, {TimeZone, Start}, Num, Acc); + deactivate -> + FSeconds = calendar:datetime_to_gregorian_seconds(Start) - 1, + NewStart = calendar:gregorian_seconds_to_datetime(FSeconds), + predict_cron_datetime(Start, End, Job, {TimeZone, NewStart}, Num, Acc); running -> predict_cron_datetime(Start, End, Job, {TimeZone, Next}, Num - 1, [to_rfc3339(TimeZone, Next) | Acc]) From 078b163a4fb712133365716f93585345aaa24c85 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Thu, 4 Mar 2021 13:58:06 +0800 Subject: [PATCH 05/11] refactor start end time --- README.md | 8 +- rebar.config | 2 +- src/ecron.app.src | 8 +- src/ecron.erl | 452 ++++++++++++++------------- src/ecron_spec.erl | 71 +++-- src/ecron_sup.erl | 55 ++-- test/prop_ecron.erl | 98 ++++-- test/prop_ecron_basic_SUITE.erl | 76 +++++ test/prop_ecron_parse.erl | 7 - test/prop_ecron_predict_datetime.erl | 106 ++----- test/prop_ecron_server.erl | 92 +++--- test/prop_ecron_spec.erl | 24 +- test/prop_ecron_status.erl | 98 +----- 13 files changed, 581 insertions(+), 516 deletions(-) create mode 100644 test/prop_ecron_basic_SUITE.erl diff --git a/README.md b/README.md index 55c81d2..4b386a8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ You can find a collection of general practices in [Full Erlang Examples](https:/ ```elixir # mix.exs def deps do - [{:ecron, "~> 0.5"}] + [{:ecron, "~> 0.6"}] end ``` @@ -63,8 +63,8 @@ You can find a collection of general practices in [Full Erlang Examples](https:/ {extend_crontab_job, "0 0 1-6/2,18 * * *", {io, format, ["Runs on 1,3,6,18 o'clock:~n"]}}, {alphabet_job, "@hourly", {io, format, ["Runs every(0-23) o'clock~n"]}}, {fixed_interval_job, "@every 30m", {io, format, ["Runs every 30 minutes"]}}, - %% Runs 0-23 o'clock since {{2019,9,26},{0,0,0}}. - {limit_datetime_job, "@hourly", {io, format, ["Runs every(0-23) o'clock~n"]}, {{2019,9,26},{0,0,0}}, unlimited}, + %% Runs every 15 minutes between {8,20,0} and {23, 59, 59}. + %% {limit_time_job, "*/15 * * * *", {io, format, ["Runs 0, 15, 30, 45 minutes after 8:20am~n"]}, {8,20,0}, unlimited} %% parallel job {no_singleton_job, "@minutely", {timer, sleep, [61000]}, unlimited, unlimited, [{singleton, false}]} ]}, @@ -275,7 +275,7 @@ For example, "@every 1h30m10s" would indicate a schedule that activates after 1 >Note: The interval doesn't take the job runtime into account. >For example, if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, ->it also has 5 minutes of idle time between each run. +>it only has 2 minutes of idle time between each run. ## Test diff --git a/rebar.config b/rebar.config index 212dee6..3c7beb6 100644 --- a/rebar.config +++ b/rebar.config @@ -22,5 +22,5 @@ {format, [ {files, ["src/*.erl", "include/*.hrl"]}, {formatter, erlfmt_formatter}, %% The erlfmt formatter interface. - {options, #{print_width => 100, ignore_pragma => true}} %% ...or no options at all. + {options, #{print_width => 120, ignore_pragma => true}} %% ...or no options at all. ]}. diff --git a/src/ecron.app.src b/src/ecron.app.src index 44505db..315d84d 100644 --- a/src/ecron.app.src +++ b/src/ecron.app.src @@ -10,7 +10,7 @@ {global_quorum_size, 1}, %% A majority of the nodes must connect. {local_jobs, [ %% {JobName, CrontabSpec, {M, F, A}} - %% {JobName, CrontabSpec, {M, F, A}, StartDateTime, EndDateTime} + %% {JobName, CrontabSpec, {M, F, A}, StartTime, EndTime} %% CrontabSpec %% 1. "Minute Hour DayOfMonth Month DayOfWeek" %% 2. "Second Minute Hour DayOfMonth Month DayOfWeek" @@ -20,11 +20,11 @@ %% {extend_crontab_job, "0 0 1-6/2,18 * * *", {io, format, ["Runs on 1,3,6,18 o'clock:~n"]}}, %% {alphabet_job, "@hourly", {io, format, ["Runs every(0-23) o'clock~n"]}}, %% {fixed_interval_job, "@every 30m", {io, format, ["Runs every 30 minutes"]}}, - %% Runs 0-23 o'clock since {{2019,9,26},{0,0,0}}. - %% {limit_datetime_job, "@hourly", {io, format, ["Runs every(0-23) o'clock~n"]}, {{2019,9,26},{0,0,0}}, unlimited} + %% Runs every 15 minutes between {8,20,0} and {23, 59, 59}. + %% {limit_time_job, "*/15 * * * *", {io, format, ["Runs 0, 15, 30, 45 minutes after 8:20am~n"]}, {8,20,0}, unlimited} ]}, {global_jobs, [ - %% {global_job, "*/10 * * * * *", {io, format, ["Runs on 0, 15, 30, 45 seconds~n"]}} + %% {global_job, "*/15 * * * * *", {io, format, ["Runs on 0, 15, 30, 45 seconds~n"]}} ]} ]}, {modules, []}, diff --git a/src/ecron.erl b/src/ecron.erl index 65e1785..de79b05 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -2,21 +2,19 @@ -include("ecron.hrl"). --export([predict_datetime/2]). - %% API Function --export([add/3, add/6]). --export([add_with_datetime/4, add_with_datetime/5]). --export([add_with_count/3, add_with_count/4]). --export([send_interval/3, send_interval/5, send_interval/7]). +-export([add/3, add/4, add/6, add/7]). +-export([add_with_time/5, add_with_time/6]). +-export([add_with_count/3, add_with_count/5]). +-export([send_interval/3, send_interval/5, send_interval/7, send_interval/8]). -export([send_after/3]). --export([delete/1]). --export([deactivate/1, activate/1]). --export([statistic/0, statistic/1]). +-export([delete/1, delete/2]). +-export([deactivate/1, activate/1, deactivate/2, activate/2]). +-export([statistic/0, statistic/1, statistic/2]). -export([reload/0]). -export([parse_spec/2]). -%% CallBack Function +%% Callback Function -export([start_link/2, handle_call/3, handle_info/2, init/1, handle_cast/2]). -export([spawn_mfa/3, clear/0]). @@ -30,25 +28,20 @@ spec, mfa, link, - start_sec = unlimited, - end_sec = unlimited, + start_at = {0, 0, 0}, + end_at = {23, 59, 59}, max_count = unlimited }). -define(MAX_SIZE, 16). -define(SECONDS_FROM_0_TO_1970, 719528 * 86400). --define(day_of_week(Y, M, D), - (case calendar:day_of_the_week(Y, M, D) of - 7 -> 0; - D1 -> D1 - end) -). -define(MatchNameSpec(Name), [{#timer{name = '$1', _ = '_'}, [], [{'=:=', '$1', {const, Name}}]}]). %%%=================================================================== %%% API %%%=================================================================== +-type register() ::atom(). -type name() :: term(). -type crontab_spec() :: crontab() | string() | binary() | 1..4294967. @@ -88,40 +81,52 @@ -type parse_error() :: invalid_time | invalid_spec | month | day_of_month | day_of_week | hour | minute | second. --type start_datetime() :: unlimited | calendar:datetime(). --type end_datetime() :: unlimited | calendar:datetime(). +-type start_at() :: unlimited | calendar:datetime(). +-type end_at() :: unlimited | calendar:datetime(). -type option() :: {singleton, boolean()} | {max_count, pos_integer() | unlimited}. -type options() :: [option()]. -%% @equiv add(JobName, Spec, MFA, unlimited, unlimited, []) +%% @equiv add(ecron_local, JobName, Spec, MFA) -spec add(name(), crontab_spec(), mfargs()) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. add(JobName, Spec, MFA) -> - add(JobName, Spec, MFA, unlimited, unlimited, []). + add(?LocalJob, JobName, Spec, MFA). -%% @equiv add_with_count(make_ref(), Spec, MFA, RunCount) +%% @equiv add(Register, JobName, Spec, MFA, unlimited, unlimited, []) +-spec add(register(), name(), crontab_spec(), mfargs()) -> + {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. +add(Register, JobName, Spec, MFA) -> + add(Register, JobName, Spec, MFA, unlimited, unlimited, []). + +%% @equiv add_with_count(ecron_local, make_ref(), Spec, MFA, RunCount) -spec add_with_count(crontab_spec(), mfargs(), pos_integer()) -> {ok, name()} | {error, parse_error(), term()}. add_with_count(Spec, MFA, RunCount) when is_integer(RunCount) -> - add_with_count(make_ref(), Spec, MFA, RunCount). + add_with_count(?LocalJob, make_ref(), Spec, MFA, RunCount). + +%% @equiv add(register(), make_ref(), Spec, MFA, unlimited, unlimited, [{max_count, RunCount}]) +-spec add_with_count(register(), name(), crontab_spec(), mfargs(), pos_integer()) -> + {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. +add_with_count(Register, JobName, Spec, MFA, RunCount) when is_integer(RunCount) -> + add(Register, JobName, Spec, MFA, unlimited, unlimited, [{max_count, RunCount}]). -%% @equiv add(make_ref(), Spec, MFA, unlimited, unlimited, [{max_count, RunCount}]) --spec add_with_count(name(), crontab_spec(), mfargs(), pos_integer()) -> +%% @equiv add(ecron_local, name(), Spec, MFA, Start, End, []) +-spec add_with_time(name(), crontab_spec(), mfargs(), start_at(), end_at()) -> {ok, name()} | {error, parse_error(), term()}. -add_with_count(JobName, Spec, MFA, RunCount) when is_integer(RunCount) -> - add(JobName, Spec, MFA, unlimited, unlimited, [{max_count, RunCount}]). +add_with_time(JobName, Spec, MFA, Start, End) -> + add_with_time(?LocalJob, JobName, Spec, MFA, Start, End). -%% @equiv add(make_ref(), Spec, MFA, Start, End, []) --spec add_with_datetime(crontab_spec(), mfargs(), start_datetime(), end_datetime()) -> +%% @equiv add(register(), name(), Spec, MFA, Start, End, []) +-spec add_with_time(register(), name(), crontab_spec(), mfargs(), start_at(), end_at()) -> {ok, name()} | {error, parse_error(), term()}. -add_with_datetime(Spec, MFA, Start, End) -> - add(make_ref(), Spec, MFA, Start, End, []). +add_with_time(Register, JobName, Spec, MFA, Start, End) -> + add(Register, JobName, Spec, MFA, Start, End, []). -%% @equiv add(JobName, Spec, MFA, Start, End, []) --spec add_with_datetime(name(), crontab_spec(), mfargs(), start_datetime(), end_datetime()) -> +%% @equiv add(ecron_local, name(), Spec, MFA, Start, End, []) +-spec add(name(), crontab_spec(), mfargs(), start_at(), end_at(), options()) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. -add_with_datetime(JobName, Spec, MFA, Start, End) -> - add(JobName, Spec, MFA, Start, End, []). +add(JobName, Spec, MFA, Start, End, Opts) -> + add(?LocalJob, JobName, Spec, MFA, Start, End, Opts). %% @doc %% Add new crontab job. All jobs that exceed the limit will be automatically removed. @@ -136,32 +141,37 @@ add_with_datetime(JobName, Spec, MFA, Start, End) -> %% %% %% --spec add(name(), crontab_spec(), mfargs(), start_datetime(), end_datetime(), options()) -> +-spec add(register(), name(), crontab_spec(), mfargs(), start_at(), end_at(), options()) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. -add(JobName, Spec, MFA, Start, End, Opts) -> - case ecron_spec:is_start_end_datetime(Start, End) of - true -> +add(Register, JobName, Spec, MFA, Start, End, Opts) -> + case ecron_spec:parse_start_end_time(Start, End) of + {StartTime, EndTime} -> case ecron_spec:parse_spec(Spec) of {ok, Type, Crontab} -> - Job = #{ - type => Type, - name => JobName, - crontab => Crontab, - mfa => MFA, - start_time => Start, - end_time => End - }, - ValidOpts = ecron_spec:parse_valid_opts(Opts), - gen_server:call(?LocalJob, {add, Job, ValidOpts}, infinity); + case ecron_spec:valid_time(Type, StartTime, EndTime, Crontab) of + true -> + Job = #{ + type => Type, + name => JobName, + crontab => Crontab, + mfa => MFA, + start_time => StartTime, + end_time => EndTime + }, + ValidOpts = ecron_spec:parse_valid_opts(Opts), + gen_server:call(Register, {add, Job, ValidOpts}, infinity); + false -> + {error, invalid_time, {Start, End, Spec}} + end; ErrParse -> ErrParse end; false -> - {error, invalid_time, {Start, End}} + {error, invalid_time, {Start, End, Spec}} end. %% @doc -%% Starts a timer which will send the message Msg to Dest when crontab is triggered. +%% Starts a timer which will send message to destination when crontab is triggered. %%
      %%
    • Equivalent to `erlang:send_after/3' expect the `Time' format.
    • %%
    • If Dest is a pid() it has to be a pid() of a local process, dead or alive.
    • @@ -170,28 +180,34 @@ add(JobName, Spec, MFA, Start, End, Opts) -> %%
    • If Dest is a pid(), the timer will be automatically canceled if the process referred to by the pid() is not alive, or when the process exits.
    • %%
    • Warning: Cancels a timer by `erlang:cancel_timer(Ref)' not `ecron:delete/1'.
    • %%
    --spec send_after(crontab_spec(), pid() | atom(), term()) -> - {ok, reference()} | {error, parse_error(), term()}. +-spec send_after(crontab_spec(), pid() | atom(), term()) -> {ok, reference()} | {error, parse_error(), term()}. send_after(Spec, Pid, Message) -> - case parse_spec(Spec, 1) of - {ok, #{next := [Next]}} -> - NextMs = calendar:rfc3339_to_system_time(Next, [{'unit', second}]) * 1000, - Time = NextMs - erlang:system_time(millisecond), - {ok, erlang:send_after(Time, Pid, Message)}; - Err -> - Err + case ecron_spec:parse_spec(Spec) of + {ok, Type, JobSpec} -> + TimeZone = get_time_zone(), + Now = current_millisecond(), + Next = next_schedule_millisecond(Type, JobSpec, TimeZone, Now, {0, 0, 0}, {23, 59, 59}), + {ok, erlang:send_after(Next - Now, Pid, Message)}; + {error, _Field, _Value} = Error -> + Error end. -%% @equiv send_interval(make_ref(), Spec, Pid, Message, unlimited, unlimited, []) +%% @equiv send_interval(ecron_local, make_ref(), Spec, Pid, Message, unlimited, unlimited, []) -spec send_interval(crontab_spec(), pid(), term()) -> {ok, name()} | {error, parse_error(), term()}. send_interval(Spec, Pid, Message) -> - send_interval(make_ref(), Spec, Pid, Message, unlimited, unlimited, []). + send_interval(?LocalJob, make_ref(), Spec, Pid, Message). -%% @equiv send_interval(make_ref(), Spec, self(), Message, Start, End, Option) --spec send_interval(crontab_spec(), term(), start_datetime(), end_datetime(), options()) -> - {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. -send_interval(Spec, Message, Start, End, Option) -> - send_interval(make_ref(), Spec, self(), Message, Start, End, Option). +%% @equiv send_interval(ecron_local,name(), Spec, Pid, Message, unlimited, unlimited, []) +-spec send_interval(register(), name(), crontab_spec(), pid(), term()) -> + {ok, name()} | {error, parse_error(), term()}. +send_interval(Register, Name, Spec, Pid, Message) -> + send_interval(Register, Name, Spec, Pid, Message, unlimited, unlimited, []). + +%% @equiv send_interval(register(), name(), Spec, self(), Message, Start, End, Option) +-spec send_interval(register(), name(), crontab_spec(), term(), start_at(), end_at(), options()) -> + {ok, name()} | {error, parse_error(), term()}. +send_interval(Register, Name, Spec, Message, Start, End, Option) -> + send_interval(Register, Name, Spec, self(), Message, Start, End, Option). %% @doc %% Evaluates Pid ! Message repeatedly when crontab is triggered. @@ -208,42 +224,48 @@ send_interval(Spec, Message, Start, End, Option) -> %% %% %% --spec send_interval( - name(), - crontab_spec(), - pid(), - term(), - start_datetime(), - end_datetime(), - options() -) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. -send_interval(JobName, Spec, Pid, Message, Start, End, Option) -> - add(JobName, Spec, {erlang, send, [Pid, Message]}, Start, End, Option). +-spec send_interval(register(), name(), crontab_spec(), pid(), term(), start_at(), end_at(), options()) -> + {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. +send_interval(Register, JobName, Spec, Pid, Message, Start, End, Option) -> + add(Register, JobName, Spec, {erlang, send, [Pid, Message]}, Start, End, Option). -%% @doc -%% Delete an exist job, if the job is nonexistent, nothing happened. +%% @equiv delete(ecron_local, Name). -spec delete(name()) -> ok. -delete(JobName) -> gen_server:call(?LocalJob, {delete, JobName}, infinity). +delete(JobName) -> delete(?LocalJob, JobName). %% @doc -%% deactivate an exist job, if the job is nonexistent, return `{error, not_found}'. -%% just freeze the job, use @see activate/1 to unfreeze job. +%% Delete an exist job, if the job is nonexistent, nothing happened. +-spec delete(register(), name()) -> ok. +delete(Register, JobName) -> gen_server:call(Register, {delete, JobName}, infinity). + +%% @equiv deactivate(ecron_local, Name). -spec deactivate(name()) -> ok | {error, not_found}. -deactivate(JobName) -> gen_server:call(?LocalJob, {deactivate, JobName}, infinity). +deactivate(JobName) -> deactivate(?LocalJob, JobName). +%% @doc +%% Deactivate an exist job, if the job is nonexistent, return `{error, not_found}'. +%% just freeze the job, use @see activate/2 to unfreeze job. +-spec deactivate(register(), name()) -> ok | {error, not_found}. +deactivate(Register, JobName) -> gen_server:call(Register, {deactivate, JobName}, infinity). +%% @equiv activate(ecron_local, Name). +-spec activate(name()) -> ok | {error, not_found}. +activate(JobName) -> activate(?LocalJob, JobName). %% @doc -%% activate an exist job, if the job is nonexistent, return `{error, not_found}'. +%% Activate an exist job, if the job is nonexistent, return `{error, not_found}'. %% if the job is already activate, nothing happened. %% the same effect as reinstall the job from now on. --spec activate(name()) -> ok | {error, not_found}. -activate(JobName) -> gen_server:call(?LocalJob, {activate, JobName}, infinity). +-spec activate(register(), name()) -> ok | {error, not_found}. +activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, infinity). +%% @equiv statistic(ecron_local,Name). +-spec statistic(name()) -> {ok, statistic()} | {error, not_found}. +statistic(JobName) -> statistic(?LocalJob, JobName). %% @doc %% Statistic from an exist job. %% if the job is nonexistent, return `{error, not_found}'. --spec statistic(name()) -> {ok, statistic()} | {error, not_found}. -statistic(JobName) -> - case ets:lookup(?LocalJob, JobName) of +-spec statistic(register(), name()) -> {ok, statistic()} | {error, not_found}. +statistic(Register, JobName) -> + case ets:lookup(Register, JobName) of [Job] -> {ok, job_to_statistic(Job)}; [] -> @@ -270,25 +292,25 @@ statistic() -> Local ++ Global. %% @doc -%% reload task manually, such as you should reload manually when the system time has alter a lot. +%% Reload task manually, such as you should reload manually when the system time has alter a lot. -spec reload() -> ok. reload() -> gen_server:cast(?LocalJob, reload), - gen_server:cast({global, ecron_global}, reload). + gen_server:cast({global, ?GlobalJob}, reload). %% @doc -%% parse a crontab spec with next trigger time. For debug. +%% Parse a crontab spec with next trigger time. For debug. -spec parse_spec(crontab_spec(), pos_integer()) -> {ok, #{type => cron | every, crontab => crontab_spec(), next => [calendar:rfc3339_string()]}} | {error, atom(), term()}. parse_spec(Spec, Num) when is_integer(Num) andalso Num > 0 -> - parse_spec_2(ecron_spec:parse_spec(Spec), Num). + parse_spec2(ecron_spec:parse_spec(Spec), Num). -parse_spec_2({ok, Type, JobSpec}, Num) -> +parse_spec2({ok, Type, JobSpec}, Num) -> Job = #{type => Type, crontab => JobSpec}, Next = predict_datetime(Job, Num), {ok, Job#{next => Next}}; -parse_spec_2({error, _Field, _Value} = Error, _Num) -> +parse_spec2({error, _Field, _Value} = Error, _Num) -> Error. get_next_schedule_time(Name) -> gen_server:call(?LocalJob, {next_schedule_time, Name}, infinity). @@ -298,7 +320,7 @@ clear() -> gen_server:call(?LocalJob, clear, infinity). predict_datetime(Job, Num) -> TZ = get_time_zone(), Now = current_millisecond(), - predict_datetime(activate, Job, unlimited, unlimited, Num, TZ, Now). + predict_datetime(activate, Job, {0, 0, 0}, {23, 59, 59}, Num, TZ, Now). %%%=================================================================== %%% CallBack @@ -306,7 +328,7 @@ predict_datetime(Job, Num) -> start_link({_, JobTab} = Name, JobSpec) -> case ecron_spec:parse_crontab(JobSpec, []) of {ok, Jobs} -> - new_job_tab(JobTab), + new_job_tab(JobTab, ets:info(JobTab)), gen_server:start_link(Name, ?MODULE, [JobTab, Jobs], []); {error, Reason} -> {error, Reason} @@ -318,16 +340,9 @@ init([JobTab, Jobs]) -> TimeZone = get_time_zone(), MaxTimeout = application:get_env(?Ecron, adjusting_time_second, 7 * 24 * 3600) * 1000, + [ets:insert_new(JobTab, Job) || Job <- Jobs], [ - begin - ets:insert_new(JobTab, Job) - end - || Job <- Jobs - ], - [ - begin - add_job(JobTab, TimerTab, Job, TimeZone, Opts, true) - end + add_job(JobTab, TimerTab, Job, TimeZone, Opts, true) || #job{job = Job, opts = Opts, status = activate} <- ets:tab2list(JobTab) ], @@ -359,15 +374,11 @@ handle_call({statistic, Name}, _From, State) -> Reply = job_to_statistic(Name, State), {reply, Reply, State, next_timeout(State)}; handle_call(statistic, _From, State = #state{timer_tab = TimerTab}) -> - Reply = - ets:foldl( - fun(#timer{name = Name}, Acc) -> - {ok, Item} = job_to_statistic(Name, State), - [Item | Acc] - end, - [], - TimerTab - ), + Fun = fun(#timer{name = Name}, Acc) -> + {ok, Item} = job_to_statistic(Name, State), + [Item | Acc] + end, + Reply = ets:foldl(Fun, [], TimerTab), {reply, Reply, State, next_timeout(State)}; handle_call({next_schedule_time, Name}, _From, State = #state{timer_tab = TimerTab}) -> Reply = get_next_schedule_time(TimerTab, Name), @@ -393,11 +404,10 @@ handle_cast(_Unknown, State) -> %%%=================================================================== %%% First Internal Functions %%%=================================================================== -new_job_tab(JobTab) -> - case ets:info(JobTab) of - undefined -> ets:new(JobTab, [named_table, set, public, {keypos, #job.name}]); - _ -> ok - end. +new_job_tab(JobTab, undefined) -> + ets:new(JobTab, [named_table, set, public, {keypos, #job.name}]); +new_job_tab(_JobTab, _Info) -> + ok. add_job(JobTab, TimerTab, Job, TimeZone, Opts, ForceUpdate) -> #{name := Name, mfa := MFA} = Job, @@ -406,18 +416,16 @@ add_job(JobTab, TimerTab, Job, TimeZone, Opts, ForceUpdate) -> IsNew = ets:insert_new(JobTab, JobRec), case IsNew orelse ForceUpdate of true -> - Singleton = proplists:get_value(singleton, Opts), - MaxCount = proplists:get_value(max_count, Opts), InitTimer = #timer{ - singleton = Singleton, - max_count = MaxCount, + singleton = proplists:get_value(singleton, Opts), + max_count = proplists:get_value(max_count, Opts), name = Name, mfa = MFA, link = PidOrUndef }, Now = current_millisecond(), telemetry:execute(?Activate, #{action_ms => Now}, #{name => Name, mfa => MFA}), - update_timer(Now, InitTimer, Job, TimerTab, JobTab, TimeZone); + update_timer(Now, InitTimer, Job, TimerTab, TimeZone); false -> {error, already_exist} end. @@ -458,33 +466,41 @@ delete_job(JobTab, TimerTab, Name) -> %%%=================================================================== %%% Second Internal Functions %%%=================================================================== -update_timer(Now, InitTimer, Job, TimeTab, JobTab, TimeZone) -> - #{name := Name, crontab := Spec, type := Type, start_time := StartTime, end_time := EndTime} = - Job, - Start = datetime_to_millisecond(TimeZone, StartTime), - End = datetime_to_millisecond(TimeZone, EndTime), - case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of - {ok, NextSec} -> - Timer = InitTimer#timer{ - key = {NextSec, Name}, - type = Type, - spec = Spec, - start_sec = Start, - end_sec = End - }, - ets:insert(TimeTab, Timer), - {ok, Name}; - {error, already_ended} = Err -> - delete_job(JobTab, TimeTab, Name), - Err - end. +update_timer(Now, InitTimer, Job, TimeTab, TimeZone) -> + #{ + name := Name, + crontab := Spec, + type := Type, + start_time := Start, + end_time := End + } = Job, + NextSec = next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End), + Timer = InitTimer#timer{ + key = {NextSec, Name}, + type = Type, + spec = Spec, + start_at = Start, + end_at = End + }, + ets:insert(TimeTab, Timer), + {ok, Name}. -next_schedule_millisecond(every, Sec, _TimeZone, Now, Start, End) -> +next_schedule_millisecond(every, Sec, TimeZone, Now, Start, End) -> Next = Now + Sec * 1000, - case in_range(Next, Start, End) of - {error, deactivate} -> {ok, Start}; - {error, already_ended} -> {error, already_ended}; - ok -> {ok, Next} + case Start =:= {0, 0, 0} andalso End =:= {23, 59, 59} of + true -> + Next; + false -> + {_, {NowHour, NowMin, NowSec}} = millisecond_to_datetime(TimeZone, Next), + {StartHour, StartMin, StartSec} = Start, + {EndHour, EndMin, EndSec} = End, + NowTime = NowHour * 3600 + NowMin * 60 + NowSec, + StartTime = StartHour * 3600 + StartMin * 60 + StartSec, + EndTime = EndHour * 3600 + EndMin * 60 + EndSec, + case NowTime >= StartTime andalso NowTime =< EndTime of + true -> Next; + false -> next_schedule_millisecond(every, Sec, TimeZone, Next, Start, End) + end end; next_schedule_millisecond(cron, Spec, TimeZone, Now, Start, End) -> ForwardDateTime = millisecond_to_datetime(TimeZone, Now + 1000), @@ -496,19 +512,26 @@ next_schedule_millisecond(cron, Spec, TimeZone, Now, Start, End) -> month => 1, day_of_week => 0 }, - Min = spec_min(maps:to_list(Spec), DefaultMin), - NextDateTime = next_schedule_datetime(Spec, Min, ForwardDateTime), - Next = datetime_to_millisecond(TimeZone, NextDateTime), - case in_range(Next, Start, End) of - {error, deactivate} -> - next_schedule_millisecond(cron, Spec, TimeZone, Start - 1000, Start, End); - {error, already_ended} -> - {error, already_ended}; - ok -> - {ok, Next} + MinSpec = spec_min(maps:to_list(Spec), DefaultMin), + next_schedule_millisecond2(Spec, MinSpec, ForwardDateTime, Start, End, TimeZone). + +next_schedule_millisecond2(Spec, MinSpec, ForwardDateTime, Start, End, TimeZone) -> + NextDateTime = {_, {NH, NM, NS}} + = next_schedule_datetime(Spec, MinSpec, ForwardDateTime, Start, End), + Next = NH * 3600 + NM * 60 + NS, + {SHour, SMin, SSec} = Start, + {EHour, EMin, ESec} = End, + case + Next >= SHour * 3600 + SMin * 60 + SSec andalso + Next =< EHour * 3600 + EMin * 60 + ESec + of + true -> + datetime_to_millisecond(TimeZone, NextDateTime); + false -> + next_schedule_millisecond2(Spec, MinSpec, NextDateTime, Start, End, TimeZone) end. -next_schedule_datetime(DateSpec, Min, DateTime) -> +next_schedule_datetime(DateSpec, Min, DateTime, Start, End) -> #{ second := SecondSpec, minute := MinuteSpec, @@ -517,27 +540,45 @@ next_schedule_datetime(DateSpec, Min, DateTime) -> month := MonthSpec, day_of_week := DayOfWeekSpec } = DateSpec, - {{Year, Month, Day}, {Hour, Minute, Second}} = DateTime, + {SHour, SMin, SSec} = Start, + {EHour, EMin, ESec} = End, + {{Year, Month, Day}, {Hour, Minute, Sec}} = DateTime, case valid_datetime(MonthSpec, Month) of false -> forward_month(DateTime, Min, DateSpec); true -> case valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) of false -> - LastDay = calendar:last_day_of_the_month(Year, Month), - forward_day(DateTime, Min, LastDay, DateSpec); + forward_day(DateTime, Min, DateSpec); true -> case valid_datetime(HourSpec, Hour) of false -> forward_hour(DateTime, Min, DateSpec); + true when Hour < SHour -> + Begin = {{Year, Month, Day}, {SHour - 1, SMin, SSec}}, + forward_hour(Begin, Min, DateSpec); + true when Hour > EHour -> + forward_day(DateTime, Min, DateSpec); true -> case valid_datetime(MinuteSpec, Minute) of false -> forward_minute(DateTime, Min, DateSpec); + true when Hour =:= SHour andalso Minute < SMin -> + Begin = {{Year, Month, Day}, {Hour, SMin - 1, SSec}}, + forward_minute(Begin, Min, DateSpec); + true when Hour =:= EHour andalso Minute > EMin -> + forward_day(DateTime, Min, DateSpec); true -> - case valid_datetime(SecondSpec, Second) of - false -> forward_second(DateTime, Min, DateSpec); - true -> DateTime + case valid_datetime(SecondSpec, Sec) of + false -> + forward_second(DateTime, Min, DateSpec); + true when Hour =:= SHour andalso Minute =:= SMin andalso Sec < SSec -> + Begin = {{Year, Month, Day}, {Hour, SMin, SSec - 1}}, + forward_second(Begin, Min, DateSpec); + true when Hour =:= EHour andalso Minute =:= EMin andalso Sec > ESec -> + forward_day(DateTime, Min, DateSpec); + true -> + DateTime end end end @@ -579,15 +620,11 @@ maybe_spawn_worker(false, Singleton, _Name, _MFA, _JobTab) -> update_next_schedule(Max, Max, _Cron, _Cur, Name, _TZ, _CurPid, Tab, JobTab) -> delete_job(JobTab, Tab, Name); -update_next_schedule(Count, _Max, Cron, Cur, Name, TZ, CurPid, Tab, JobTab) -> - #timer{type = Type, start_sec = Start, end_sec = End, spec = Spec} = Cron, - case next_schedule_millisecond(Type, Spec, TZ, Cur, Start, End) of - {ok, Next} -> - NextTimer = Cron#timer{key = {Next, Name}, singleton = CurPid, cur_count = Count}, - ets:insert(Tab, NextTimer); - {error, already_ended} -> - delete_job(JobTab, Tab, Name) - end. +update_next_schedule(Count, _Max, Cron, Cur, Name, TZ, CurPid, Tab, _JobTab) -> + #timer{type = Type, start_at = Start, end_at = End, spec = Spec} = Cron, + Next = next_schedule_millisecond(Type, Spec, TZ, Cur, Start, End), + NextTimer = Cron#timer{key = {Next, Name}, singleton = CurPid, cur_count = Count}, + ets:insert(Tab, NextTimer). spawn_mfa(JobTab, Name, MFA) -> Start = erlang:monotonic_time(), @@ -650,13 +687,17 @@ forward_hour(DateTime, Min, Spec) -> NewHour = nearest(hour, Hour, 23, Spec), case Hour >= NewHour of true -> - LastDay = calendar:last_day_of_the_month(Year, Month), - forward_day(DateTime, Min, LastDay, Spec); + forward_day(DateTime, Min, Spec); false -> #{minute := MinuteM, second := SecondM} = Min, {{Year, Month, Day}, {NewHour, MinuteM, SecondM}} end. +forward_day(DateTime, Min, Spec) -> + {{Year, Month, _Day}, _} = DateTime, + LastDay = calendar:last_day_of_the_month(Year, Month), + forward_day(DateTime, Min, LastDay, Spec). + forward_day(DateTime, Min, LastDay, Spec) -> {{Year, Month, Day}, {_Hour, _Minute, _Second}} = DateTime, case Day + 1 of @@ -685,14 +726,11 @@ forward_month(DateTime, Min, Spec) -> #{day_of_week := DayOfWeekSpec, day_of_month := DayOfMonthSpec} = Spec, case valid_day(NYear, NMonth, NDay, DayOfMonthSpec, DayOfWeekSpec) of false -> - LastDay = calendar:last_day_of_the_month(NYear, NMonth), - forward_day(NewDateTime, Min, LastDay, Spec); + forward_day(NewDateTime, Min, Spec); true -> NewDateTime end. -datetime_to_millisecond(_, unlimited) -> - unlimited; datetime_to_millisecond(local, DateTime) -> UtcTime = erlang:localtime_to_universaltime(DateTime), datetime_to_millisecond(utc, UtcTime); @@ -737,12 +775,12 @@ valid_day(_Year, _Month, _Day, '*', '*') -> valid_day(_Year, _Month, Day, DayOfMonthSpec, '*') -> valid_datetime(DayOfMonthSpec, Day); valid_day(Year, Month, Day, '*', DayOfWeekSpec) -> - DayOfWeek = ?day_of_week(Year, Month, Day), + DayOfWeek = day_of_week(Year, Month, Day), valid_datetime(DayOfWeekSpec, DayOfWeek); valid_day(Year, Month, Day, DayOfMonthSpec, DayOfWeekSpec) -> case valid_datetime(DayOfMonthSpec, Day) of false -> - DayOfWeek = ?day_of_week(Year, Month, Day), + DayOfWeek = day_of_week(Year, Month, Day), valid_datetime(DayOfWeekSpec, DayOfWeek); true -> true @@ -765,16 +803,6 @@ next_timeout(#state{timer_tab = TimerTab, max_timeout = MaxTimeout}) -> {Due, _} -> min(max(Due - current_millisecond(), 0), MaxTimeout) end. -in_range(_Current, unlimited, unlimited) -> ok; -in_range(Current, unlimited, End) when Current > End -> {error, already_ended}; -in_range(_Current, unlimited, _End) -> ok; -in_range(Current, Start, unlimited) when Current < Start -> {error, deactivate}; -in_range(_Current, _Start, unlimited) -> ok; -in_range(Current, _Start, End) when Current > End -> {error, already_ended}; -in_range(Current, Start, _End) when Current < Start -> {error, deactivate}; -in_range(_Current, _Start, _End) -> ok. - -to_rfc3339(unlimited) -> unlimited; to_rfc3339(Next) -> calendar:system_time_to_rfc3339(Next div 1000, [{unit, second}]). predict_datetime(deactivate, _, _, _, _, _, _) -> @@ -785,21 +813,17 @@ predict_datetime(activate, #{type := every, crontab := Sec} = Job, Start, End, N error -> NowT; _ -> NowT - Sec * 1000 end, - predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []); + predict_datetime2(Job, TimeZone, Now, Start, End, Num, []); predict_datetime(activate, Job, Start, End, Num, TimeZone, Now) -> - predict_datetime_2(Job, TimeZone, Now, Start, End, Num, []). + predict_datetime2(Job, TimeZone, Now, Start, End, Num, []). -predict_datetime_2(_Job, _TimeZone, _Now, _Start, _End, 0, Acc) -> +predict_datetime2(_Job, _TimeZone, _Now, _Start, _End, 0, Acc) -> lists:reverse(Acc); -predict_datetime_2(Job, TimeZone, Now, Start, End, Num, Acc) -> +predict_datetime2(Job, TimeZone, Now, Start, End, Num, Acc) -> #{type := Type, crontab := Spec} = Job, - case next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End) of - {ok, Next} -> - NewAcc = [to_rfc3339(Next) | Acc], - predict_datetime_2(Job, TimeZone, Next, Start, End, Num - 1, NewAcc); - {error, already_ended} -> - lists:reverse(Acc) - end. + Next = next_schedule_millisecond(Type, Spec, TimeZone, Now, Start, End), + NewAcc = [to_rfc3339(Next) | Acc], + predict_datetime2(Job, TimeZone, Next, Start, End, Num - 1, NewAcc). get_next_schedule_time(Timer, Name) -> %% P = ets:fun2ms(fun(#timer{name = N, key = {Time, _}}) when N =:= Name -> Time end), @@ -856,21 +880,25 @@ job_to_statistic(Job, TimeZone, Now) -> run_microsecond = RunMs } = Job, #{start_time := StartTime, end_time := EndTime} = JobSpec, - Start = datetime_to_millisecond(TimeZone, StartTime), - End = datetime_to_millisecond(TimeZone, EndTime), JobSpec#{ status => Status, ok => Ok, failed => Failed, opts => Opts, - next => predict_datetime(Status, JobSpec, Start, End, ?MAX_SIZE, TimeZone, Now), - start_time => to_rfc3339(datetime_to_millisecond(TimeZone, StartTime)), - end_time => to_rfc3339(datetime_to_millisecond(TimeZone, EndTime)), + next => predict_datetime(Status, JobSpec, StartTime, EndTime, ?MAX_SIZE, TimeZone, Now), + start_time => StartTime, + end_time => EndTime, node => node(), results => Res, run_microsecond => RunMs }. +day_of_week(Y, M, D) -> + case calendar:day_of_the_week(Y, M, D) of + 7 -> 0; + D1 -> D1 + end. + %% For PropEr Test -ifdef(TEST). -compile(export_all). diff --git a/src/ecron_spec.erl b/src/ecron_spec.erl index dec2841..e8ae64f 100644 --- a/src/ecron_spec.erl +++ b/src/ecron_spec.erl @@ -6,11 +6,36 @@ %% API -export([parse_spec/1]). -export([parse_crontab/2]). --export([is_start_end_datetime/2]). +-export([parse_start_end_time/2]). -export([parse_valid_opts/1]). +-export([valid_time/4]). %% @private +valid_time(cron, {SH, SM, SS}, {EH, EM, ES}, CronTab) -> + Start = SH * 3600 + SM * 60 + SS, + End = EH * 3600 + EM * 60 + ES, + case End > Start of + false -> + false; + true -> + #{hour := HourSpec, minute := MinuteSpec, second := SecondSpec} = CronTab, + Hour = parse_max(23, HourSpec), + Minute = parse_max(59, MinuteSpec), + Second = parse_max(59, SecondSpec), + Spec = Hour * 3600 + Minute * 60 + Second, + Spec >= Start andalso Spec =< End + end; +valid_time(every, {SH, SM, SS}, {EH, EM, ES}, _CronTab) -> + ((EH - SH) * 3600 + (EM - SM) * 60 + (ES - SS)) >= 0. + +parse_max(Max, '*') -> Max; +parse_max(_DefaultMax, List) -> + case lists:last(List) of + {_, Max} -> Max; + Max -> Max + end. + % Run once a year, midnight, Jan. 1st parse_spec("@yearly") -> parse_spec("0 0 0 1 1 *"); @@ -322,8 +347,8 @@ parse_crontab([L | _], _Acc) -> {error, L}. parse_job(JobName, Spec, MFA, Start, End, Opts) -> - case is_start_end_datetime(Start, End) of - true -> + case parse_start_end_time(Start, End) of + {StartTime, EndTime} -> case parse_spec(Spec) of {ok, Type, Crontab} -> Job = #{ @@ -331,8 +356,8 @@ parse_job(JobName, Spec, MFA, Start, End, Opts) -> name => JobName, crontab => Crontab, mfa => MFA, - start_time => Start, - end_time => End + start_time => StartTime, + end_time => EndTime }, {ok, #job{ name = JobName, @@ -352,24 +377,32 @@ parse_valid_opts(Opts) -> MaxCount = proplists:get_value(max_count, Opts, unlimited), [{singleton, Singleton}, {max_count, MaxCount}]. -is_start_end_datetime(Start, End) -> - case is_datetime(Start) andalso is_datetime(End) of - true when Start =/= unlimited andalso End =/= unlimited -> - EndSec = calendar:datetime_to_gregorian_seconds(End), - StartSec = calendar:datetime_to_gregorian_seconds(Start), - EndSec > StartSec; - Res -> - Res +parse_start_end_time(Start, End) -> + case {Start, End} of + {unlimited, unlimited} -> + {{0, 0, 0}, {23, 59, 59}}; + {unlimited, _} -> + case datetime(End) of + false -> false; + true -> {{0, 0, 0}, End} + end; + {_, unlimited} -> + case datetime(Start) of + false -> false; + true -> {Start, {23, 59, 59}} + end; + _ -> + case datetime(Start) andalso datetime(End) of + true -> {Start, End}; + false -> false + end end. -is_datetime(unlimited) -> - true; -is_datetime({Date, {H, M, S}}) -> +datetime({H, M, S}) -> (is_integer(H) andalso H >= 0 andalso H =< 23) andalso (is_integer(M) andalso M >= 0 andalso M =< 59) andalso - (is_integer(S) andalso S >= 0 andalso H =< 59) andalso - calendar:valid_date(Date); -is_datetime(_ErrFormat) -> + (is_integer(S) andalso S >= 0 andalso H =< 59); +datetime(_ErrFormat) -> false. %% For PropEr Test diff --git a/src/ecron_sup.erl b/src/ecron_sup.erl index 4811d8f..2ac9b23 100644 --- a/src/ecron_sup.erl +++ b/src/ecron_sup.erl @@ -16,19 +16,15 @@ start_link() -> start_global(Measurements) -> GlobalJobs = application:get_env(?Ecron, global_jobs, []), - case - supervisor:start_child( - ?MODULE, - #{ - id => ?GLOBAL_WORKER, - start => {ecron, start_link, [{global, ?GlobalJob}, GlobalJobs]}, - restart => temporary, - shutdown => 1000, - type => worker, - modules => [?GLOBAL_WORKER] - } - ) - of + GlobalSpec = #{ + id => ?GLOBAL_WORKER, + start => {ecron, start_link, [{global, ?GlobalJob}, GlobalJobs]}, + restart => temporary, + shutdown => 1000, + type => worker, + modules => [?GLOBAL_WORKER] + }, + case supervisor:start_child(?MODULE, GlobalSpec) of {ok, Pid} -> telemetry:execute(?GlobalUp, Measurements, #{self => node()}), {ok, Pid}; @@ -52,7 +48,7 @@ init([]) -> intensity => 100, period => 30 }, - Local = #{ + LocalSpec = #{ id => ?LOCAL_WORKER, start => {?LOCAL_WORKER, start_link, [{local, ?LocalJob}, LocalJobs]}, restart => permanent, @@ -60,17 +56,20 @@ init([]) -> type => worker, modules => [?LOCAL_WORKER] }, - case GlobalJobs of - [] -> - {ok, {SupFlags, [Local]}}; - _ -> - Global = #{ - id => ?MONITOR_WORKER, - start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [?MONITOR_WORKER] - }, - {ok, {SupFlags, [Local, Global]}} - end. + GlobalSpec = + case application:get_env(?Ecron, global_jobs, []) of + [] -> + []; + GlobalJobs -> + [ + #{ + id => ?MONITOR_WORKER, + start => {?MONITOR_WORKER, start_link, [{local, ?MONITOR_WORKER}, GlobalJobs]}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [?MONITOR_WORKER] + } + ] + end, + {ok, {SupFlags, [LocalSpec | GlobalSpec]}}. diff --git a/test/prop_ecron.erl b/test/prop_ecron.erl index cea095c..5dfcf10 100644 --- a/test/prop_ecron.erl +++ b/test/prop_ecron.erl @@ -5,11 +5,13 @@ -export([unzip/1]). -export([check_day_of_month/1]). -export([field_to_extend/1]). --export([to_ms/1]). +-export([to_localtime/1, to_utctime/1]). -export([field_spec_to_str/1]). -export([max_day_of_month/1]). -export([check_cron_result/4]). --export([check_every_result/4]). +-export([check_every_result/5]). +-export([valid_datetime/1]). +-export([check_predict_datetime/1]). spec_to_str(Spec) -> Format = string:join(lists:duplicate(length(Spec), "~s"), " "), @@ -26,27 +28,46 @@ cron_spec_to_map(Spec) -> day_of_week => Week }. -check_cron_result(_DateSpec, _StartTime, _EndTime, []) -> true; -check_cron_result(DateSpec, StartTime, EndTime, [T1 | Rest]) -> - T = to_ms(T1), - DateTime = calendar:system_time_to_local_time(T, millisecond), - case is_greater_than_or_equal(T, StartTime) andalso - is_greater_than_or_equal(EndTime, T) andalso +check_cron_result(_DateSpec, _Start, _End, []) -> true; +check_cron_result(DateSpec, Start, End, [T1 | Rest]) -> + DateTime = {_, Time} = to_localtime(T1), + case is_greater_than_or_equal(Time, Start) andalso + is_greater_than_or_equal(End, Time) andalso in_cron_range(DateSpec, DateTime) of - true -> check_cron_result(DateSpec, StartTime, EndTime, Rest); + true -> check_cron_result(DateSpec, Start, End, Rest); false -> false end. -check_every_result(_Second, _Start, _End, []) -> true; -check_every_result(_Second, _Start, _End, [_]) -> true; -check_every_result(Second, Start, End, [T1, T2 | Rest]) -> - T11 = to_ms(T1) div 1000, - T22 = to_ms(T2) div 1000, - case is_greater_than_or_equal(T11, Start) andalso - is_greater_than_or_equal(End, T11) andalso - (T22 - T11) =:= Second of - true -> check_every_result(Second, Start, End, [T2 | Rest]); - false -> false +check_every_result(_Second, _Start, _End, _ZT, []) -> true; +check_every_result(_Second, _Start, _End, _ZT, [_]) -> true; +check_every_result(Second, Start, End, ZT, [T1, T2 | Rest]) -> + case ZT of + utc -> + {_, {H1, M1, S1}} = to_utctime(T1), + {_, {H2, M2, S2}} = to_utctime(T2); + local -> + {_, {H1, M1, S1}} = to_localtime(T1), + {_, {H2, M2, S2}} = to_localtime(T2) + end, + {SH, SM, SS} = Start, + {EH, EM, ES} = End, + StartTime = SH*3600 + SM*60 + SS, + EndTime = EH*3600 + EM*60 + ES, + Time1 = H1*3600 + M1*60 + S1, + Time2 = H2*3600 + M2*60 + S2, + case Time1 >= StartTime andalso Time1 =< EndTime + andalso Time2 >= StartTime andalso Time2 =< EndTime of + false -> false; + true -> + T22 = calendar:rfc3339_to_system_time(T2, [{unit, second}]), + T11 = calendar:rfc3339_to_system_time(T1, [{unit, second}]), + case T22 - T11 =:= Second of + true -> check_every_result(Second, Start, End, ZT, [T2 | Rest]); + false -> + {T222, _} = calendar:system_time_to_local_time(T22, second), + {T111, _} = calendar:system_time_to_local_time(T11, second), + T222 =/= T111 + end end. field_spec_to_str("*") -> "*"; @@ -58,12 +79,42 @@ field_spec_to_str({'Min-Max/Step', Type, Min, Max, Step}) -> int_to_str(Type, Min) ++ "-" ++ int_to_str(Type, Max) ++ "/" ++ integer_to_list(Step); field_spec_to_str({list, _Type, List}) -> string:join(lists:map(fun field_spec_to_str/1, List), ","). +unzip('*') -> [0]; unzip(undefined) -> []; unzip(List) -> lists:usort(unzip_list(List, [])). unzip_list([], Acc) -> Acc; unzip_list([H | T], Acc) when is_integer(H) -> unzip_list(T, [H | Acc]); unzip_list([{Min, Max} | T], Acc) -> unzip_list(T, lists:seq(Min, Max) ++ Acc). +check_predict_datetime(Spec) -> + check_day_of_month(element(1, Spec)) andalso valid_datetime(Spec). + +valid_datetime({Spec, {SH, SM, SS}, {EH, EM, ES}}) -> + Start = SH * 3600 + SM * 60 + SS, + End = EH * 3600 + EM * 60 + ES, + case End > Start of + false -> + false; + true -> + {Second, Minute, Hour} = + case Spec of + [S, M, H, _DOM, _Month, _DOW] -> + { + lists:max(unzip(field_to_extend(S))), + lists:max(unzip(field_to_extend(M))), + lists:max(unzip(field_to_extend(H))) + }; + [M, H, _DOM, _Month, _DOW] -> + { + 59, + lists:max(unzip(field_to_extend(M))), + lists:max(unzip(field_to_extend(H))) + } + end, + Time = Hour * 3600 + Minute * 60 + Second, + Time >= Start andalso Time =< End + end. + check_day_of_month([_M, _H, _DOM, _Month, _DOW] = T) -> check_day_of_month(["0" | T]); check_day_of_month([_S, _M, _H, "*", _Month, _DOW]) -> true; check_day_of_month([_S, _M, _H, _DOM, "*", _DOW]) -> true; @@ -80,8 +131,11 @@ field_to_extend({'Min/Step', _Type, Min, MaxLimit, Step}) -> ecron_spec:zip(list field_to_extend({'Min-Max/Step', _Type, Min, Max, Step}) -> ecron_spec:zip(lists:seq(Min, Max, Step)); field_to_extend({list, _Type, List}) -> ecron_spec:zip(unzip(lists:flatten([field_to_extend(L) || L <- List]))). -to_ms(unlimited) -> unlimited; -to_ms(Time) -> calendar:rfc3339_to_system_time(Time, [{unit, millisecond}]). +to_localtime(unlimited) -> unlimited; +to_localtime(Time) -> calendar:system_time_to_local_time(calendar:rfc3339_to_system_time(Time, [{unit, millisecond}]), millisecond). + +to_utctime(unlimited) -> unlimited; +to_utctime(Time) -> calendar:system_time_to_universal_time(calendar:rfc3339_to_system_time(Time, [{unit, millisecond}]), millisecond). %%%%%%%%%%%%%%%%% %%% Internal %%% @@ -145,4 +199,4 @@ in_cron_range2(Value, ZipValues) -> lists:member(Value, prop_ecron:unzip(ZipValu is_greater_than_or_equal(unlimited, _Datetime) -> true; is_greater_than_or_equal(_, unlimited) -> true; -is_greater_than_or_equal(A, B) -> A >= B. +is_greater_than_or_equal({H1,M1,S1}, {H2,M2,S2}) -> H1*60*60 + M1*60 + S1 >= H2*60*60 + M2*60 + S2. diff --git a/test/prop_ecron_basic_SUITE.erl b/test/prop_ecron_basic_SUITE.erl new file mode 100644 index 0000000..e3fe33e --- /dev/null +++ b/test/prop_ecron_basic_SUITE.erl @@ -0,0 +1,76 @@ +-module(prop_ecron_basic_SUITE). +-include_lib("ecron/include/ecron.hrl"). + +%%% Common Test includes +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-export([all/0, suite/0, groups/0, init_per_suite/1, end_per_suite/1]). +-compile(export_all). + +-define(NAME, ?MODULE). + +suite() -> + [{timetrap, {minutes, 1}}]. + +all() -> + [ + basic,error_start_end_time + ]. + +groups() -> []. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + error_logger:tty(true), + ok. + +init_per_testcase(_TestCase, Config) -> + application:ensure_all_started(ecron), + {ok, _Pid} = ecron:start_link({local, ?NAME}, []), + Config. + +basic(_Config) -> + JobName = check_mail, + MFA = {io,format,["Mail checking~n"]}, + {ok, JobName} = ecron:add(?NAME, JobName, "0 0 8 * * 1-5", MFA, unlimited, unlimited, []), + {ok, Result} = ecron:statistic(?NAME, JobName), + #{crontab := + #{day_of_month := '*', + day_of_week := [{1,5}], + hour := "\b", + minute := [0], + month := '*', + second := [0]} = CrontabSpec, + end_time := {23,59,59}, + failed := 0, + mfa := MFA, + name := check_mail, + next := Next, + opts := [{singleton,true},{max_count,unlimited}], + start_time := {0,0,0}, + status := activate,type := cron} = Result, + true = prop_ecron:check_cron_result(CrontabSpec, {0, 0, 0}, {23, 59, 59}, Next), + ok = ecron:deactivate(?NAME, JobName), + {ok, #{status := deactivate}} = ecron:statistic(?NAME, JobName), + ok = ecron:delete(?NAME, JobName), + {error, not_found} = ecron:statistic(?NAME, JobName), + ok. + +error_start_end_time(_Config) -> + JobName = check_mail_with_time, + MFA = {io,format,["Mail checking~n"]}, + {error, invalid_time, _} = ecron:add_with_time(JobName, "0 0 8 * * 1-5", MFA, {12, 0, 0}, {11, 0, 0}), + {error, invalid_time, _} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, {9, 20, 0}, {12, 0, 0}), + {error, invalid_time, _} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, {109, 20, 0}, {12, 0, 0}), + {error, invalid_time, _} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, {9, 20, 0}, 12), + {error, invalid_time, _} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, unlimited, 1), + {ok, JobName} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, unlimited, unlimited), + ok = ecron:delete(JobName), + {ok, JobName} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, {8, 0, 0}, unlimited), + ok = ecron:delete(JobName), + {ok, JobName} = ecron:add_with_time(JobName, "0 10 8,9 * * 1-5", MFA, unlimited, {10, 0, 0}), + ok = ecron:delete(JobName), + ok. diff --git a/test/prop_ecron_parse.erl b/test/prop_ecron_parse.erl index 960a2ea..ae4c97f 100644 --- a/test/prop_ecron_parse.erl +++ b/test/prop_ecron_parse.erl @@ -4,7 +4,6 @@ -export([prop_zip/0, prop_zip/1]). -export([prop_max_day_of_months/0, prop_max_day_of_months/1]). --export([prop_valid_datetime/0, prop_valid_datetime/1]). -export([prop_spec_extend/0, prop_spec_extend/1]). -export([prop_spec_standard/0, prop_spec_standard/1]). -export([prop_spec_predefined/0, prop_spec_predefined/1]). @@ -34,12 +33,6 @@ prop_max_day_of_months() -> Expect =:= Actual end). -prop_valid_datetime(doc) -> "ecron_spec:is_start_end_ok/2 failed"; -prop_valid_datetime(opts) -> [{numtests, 4000}]. -prop_valid_datetime() -> - ?FORALL({Date, Time}, prop_ecron_spec:datetime(), - ?IMPLIES(calendar:valid_date(Date), ecron_spec:is_datetime({Date, Time}))). - prop_spec_extend(doc) -> "ecron_spec:parse_spec(\"second minute hour day_of_month month day_of_week\") failed"; prop_spec_extend(opts) -> [{numtests, 4000}]. prop_spec_extend() -> diff --git a/test/prop_ecron_predict_datetime.erl b/test/prop_ecron_predict_datetime.erl index 6b9a198..c9b8642 100644 --- a/test/prop_ecron_predict_datetime.erl +++ b/test/prop_ecron_predict_datetime.erl @@ -10,30 +10,28 @@ %%%%%%%%%%%%%%%%%% prop_predict_cron_datetime(doc) -> "predict cron datetime failed"; -prop_predict_cron_datetime(opts) -> [{numtests, 5000}]. +prop_predict_cron_datetime(opts) -> [{numtests, 8000}]. prop_predict_cron_datetime() -> - ?FORALL(Spec, {prop_ecron_spec:extend_spec(), shift(), shift()}, - ?IMPLIES(prop_ecron:check_day_of_month(element(1, Spec)), + ?FORALL(Spec, {prop_ecron_spec:extend_spec(), + {range(0, 12), range(0, 59), range(0, 59)}, + {range(13, 23), range(0, 59), range(0, 59)}}, + ?IMPLIES(prop_ecron:check_predict_datetime(Spec), begin - {CrontabSpec, StartShift, EndShift} = Spec, + {CrontabSpec, Start, End} = Spec, SpecStr = prop_ecron:spec_to_str(CrontabSpec), {ok, cron, NewCrontabSpec} = ecron_spec:parse_spec(SpecStr), Now = erlang:system_time(millisecond), - StartTime = shift_time(Now, StartShift), - EndTime = shift_time(Now, EndShift), NewSpec = #{ type => cron, crontab => NewCrontabSpec, - start_time => StartTime, - end_time => EndTime + start_time => Start, + end_time => End }, - Start = ecron:datetime_to_millisecond(local, StartTime), - End = ecron:datetime_to_millisecond(local, EndTime), List = ecron:predict_datetime(activate, NewSpec, Start, End, 500, local, Now), NowDateTime = calendar:system_time_to_local_time(Now, millisecond), - ExpectList = predict_cron_datetime(StartTime, EndTime, NewCrontabSpec, {local, NowDateTime}, 500, []), + ExpectList = predict_cron_datetime(Start, End, NewCrontabSpec, {local, NowDateTime}, 500, []), ?WHENFAIL( - io:format("Predict ~p\n ~p\nFailed: ~p~n~p~n", [SpecStr, NewSpec, StartTime, + io:format("Predict ~p\n ~p\nFailed: ~p~n~p~n", [SpecStr, NewSpec, Start, {activate, NewSpec, Start, End, 500, local}]), ExpectList =:= List andalso prop_ecron:check_cron_result(NewCrontabSpec, Start, End, List) ) @@ -43,46 +41,39 @@ prop_predict_cron_datetime() -> prop_predict_every_datetime(doc) -> "predict every datetime failed"; prop_predict_every_datetime(opts) -> [{numtests, 3000}]. prop_predict_every_datetime() -> - ?FORALL(Spec, {range(1, ?MAX_TIMEOUT), shift(), shift()}, + ?FORALL(Spec, {range(1, ?MAX_TIMEOUT), + {range(0, 12), range(0, 59), range(0, 59)}, + {range(13, 23), range(0, 59), range(0, 59)}}, begin Now = erlang:system_time(millisecond), - {Second, StartShift, EndShift} = Spec, - StartTime = shift_time(Now, StartShift), - EndTime = shift_time(Now, EndShift), + {Second, Start, End} = Spec, NewSpec = #{ type => every, crontab => Second, - start_time => StartTime, - end_time => EndTime + start_time => Start, + end_time => End }, - Start = shift_ms(Now, StartShift), - End = shift_ms(Now, EndShift), List = ecron:predict_datetime(activate, NewSpec, Start, End, 10, utc, Now), ?WHENFAIL( io:format("Predict Failed: ~p ~p~n", [NewSpec, List]), - prop_ecron:check_every_result(Second, - shift_second(Now div 1000, StartShift), - shift_second(Now div 1000, EndShift), List) + prop_ecron:check_every_result(Second, Start, End, utc, List) ) end). %%%%%%%%%%%%%%% %%% Helpers %%% %%%%%%%%%%%%%%% - predict_cron_datetime(_Start, _End, _Job, _Now, 0, Acc) -> lists:reverse(Acc); predict_cron_datetime(Start, End, Job, {TimeZone, Now}, Num, Acc) -> Next = next_schedule_datetime(Job, Now), case in_range(Next, Start, End) of - already_ended -> lists:reverse(Acc); - deactivate -> - FSeconds = calendar:datetime_to_gregorian_seconds(Start) - 1, - NewStart = calendar:gregorian_seconds_to_datetime(FSeconds), - predict_cron_datetime(Start, End, Job, {TimeZone, NewStart}, Num, Acc); - running -> + false -> + predict_cron_datetime(Start, End, Job, {TimeZone, Next}, Num, Acc); + true -> predict_cron_datetime(Start, End, Job, {TimeZone, Next}, Num - 1, [to_rfc3339(TimeZone, Next) | Acc]) end. + next_schedule_datetime(DateSpec, DateTime) -> ForwardDateTime = forward_sec(DateTime), next_schedule_datetime(forward, DateSpec, ForwardDateTime). @@ -146,46 +137,17 @@ forward_day(DateTime) -> {{Y, M, D}, _} = calendar:gregorian_seconds_to_datetime(FSeconds), {{Y, M, D}, {0, 0, 0}}. -shift_time(_, unlimited) -> unlimited; -shift_time(Current, Ms) -> calendar:system_time_to_local_time(Current + Ms * 1000, millisecond). - -shift_ms(_, unlimited) -> unlimited; -shift_ms(Current, Ms) -> Current + Ms * 1000. - -shift_second(_, unlimited) -> unlimited; -shift_second(Current, Ms) -> Current + Ms. - --define(AlreadyEndedStatus, already_ended). --define(WaitingStatus, waiting). --define(RunningStatus, running). --define(DeactivateStatus, deactivate). --define(ActivateStatus, activate). - -in_range(_Current, unlimited, unlimited) -> ?RunningStatus; -in_range(Current, unlimited, End) -> - case second_diff(End, Current) > 0 of - true -> ?AlreadyEndedStatus; - false -> ?RunningStatus - end; -in_range(Current, Start, unlimited) -> - case second_diff(Start, Current) < 0 of - true -> ?DeactivateStatus; - false -> ?RunningStatus - end; -in_range(Current, Start, End) -> - case second_diff(End, Current) > 0 of - true -> ?AlreadyEndedStatus; - false -> - case second_diff(Start, Current) < 0 of - true -> ?DeactivateStatus; - false -> ?RunningStatus - end - end. +in_range(_Current, unlimited, unlimited) -> ok; +in_range({_, {H, M, S}}, unlimited, {EH, EM, ES}) -> + H*3600 + M*60 + S >= EH *3600 + EM*60 + ES; -second_diff(CurrentDateTime, NextDateTime) -> - CurrentSeconds = calendar:datetime_to_gregorian_seconds(CurrentDateTime), - NextSeconds = calendar:datetime_to_gregorian_seconds(NextDateTime), - NextSeconds - CurrentSeconds. +in_range({_, {H, M, S}}, {SH, SM, SS}, unlimited) -> + H*3600 + M*60 + S =< SH *3600 + SM*60 + SS; +in_range({_, {H, M, S}}, {SH, SM, SS}, {EH, EM, ES}) -> + STime = SH*3600 + SM*60 + SS, + ETime = EH*3600 + EM*60 + ES, + Time = H*3600 + M*60 + S, + Time >= STime andalso Time =< ETime. valid_datetime('*', _Value) -> true; valid_datetime([], _Value) -> false; @@ -209,9 +171,3 @@ to_rfc3339(TimeZone, Time) -> %%%%%%%%%%%%%%%%%% %%% Generators %%% %%%%%%%%%%%%%%%%%% -shift() -> - frequency([ - {9, range(-3600 * 12, 3600 * 12)}, - {3, unlimited} - ]). - diff --git a/test/prop_ecron_server.erl b/test/prop_ecron_server.erl index c87fd87..55fe7c1 100644 --- a/test/prop_ecron_server.erl +++ b/test/prop_ecron_server.erl @@ -105,7 +105,8 @@ postcondition(_State, {call, _Mod, add_with_count, [_ | _] = Args}, Res) -> postcondition(_State, {call, _Mod, add_with_datetime, [_ | _] = Args}, Res) -> check_add_with_limit(Args, Res); postcondition(_State, {call, _Mod, add_cron_existing, [_Name | _]}, Res) -> - Res =:= {error, already_exist}; + element(1, Res) =:= error andalso + ( element(2, Res) =:= already_exist orelse element(2, Res) =:= invalid_time); postcondition(_State, {call, _Mod, add_every_new, [_Name | _] = Args}, Res) -> check_add_new(Args, Res); postcondition(_State, {call, _Mod, add_every_existing, [_Name | _]}, Res) -> @@ -137,26 +138,26 @@ postcondition(State, {call, _Mod, statistic_all, []}, Res) -> next_state(State, Res, {call, _Mod, add_cron_new, [Name, _Spec, _MFA] = Args}) -> State#{Name => #{cron => new_cron(Args), worker => Res}}; next_state(State, Res, {call, _Mod, add_cron_new, [Name, Spec, _MFA, {Start, End}] = Args}) -> - case is_expired(Spec, Start, End) of - true -> State; - false -> State#{Name => #{cron => new_cron(Args), worker => Res}} + case is_valid_time(Spec, Start, End) of + false -> State; + true -> State#{Name => #{cron => new_cron(Args), worker => Res}} end; next_state(State, Res, {call, _Mod, add_with_count, [Spec, MFA, _Count]}) -> Name = make_ref(), State#{Name => #{cron => new_cron([Name, Spec, MFA]), worker => Res}}; next_state(State, Res, {call, _Mod, add_with_datetime, [Spec, MFA, {Start, End}]}) -> - case is_expired(Spec, Start, End) of - true -> State; - false -> + case is_valid_time(Spec, Start, End) of + false -> State; + true -> Name = make_ref(), State#{Name => #{cron => new_cron([Name, Spec, MFA, {Start, End}]), worker => Res}} end; next_state(State, Res, {call, _Mod, add_every_new, [Name, _Spec, _MFA] = Args}) -> State#{Name => #{cron => new_every(Args), worker => Res}}; next_state(State, Res, {call, _Mod, add_every_new, [Name, Spec, _MFA, {Start, End}] = Args}) -> - case is_expired(Spec, Start, End) of - true -> State; - false -> State#{Name => #{cron => new_every(Args), worker => Res}} + case is_valid_time(Spec, Start, End) of + false -> State; + true -> State#{Name => #{cron => new_every(Args), worker => Res}} end; next_state(State, _Res, {call, _Mod, delete_existing, [Name]}) -> maps:remove(Name, State); next_state(State, _Res, {call, _Mod, deactivate_existing, [_Name]}) -> State; @@ -185,32 +186,51 @@ new_every([Name, Second, MFA, {Start, End}]) -> end_time => End, mfa => MFA }. check_add_new([Name, _Spec, _MFA | _], {ok, Name}) -> true; -check_add_new([_Name, Spec, _MFA, {StartTime, EndTime}], {error, already_ended}) -> - is_expired(Spec, StartTime, EndTime). +check_add_new([_Name, Spec, _MFA, {StartTime, EndTime}], {error, invalid_time, _}) -> + not is_valid_time(Spec, StartTime, EndTime). check_add_with_limit([_Spec, _MFA | _], {ok, _Name}) -> true; -check_add_with_limit([Spec, _MFA, {StartTime, EndTime}], {error, already_ended}) -> - is_expired(Spec, StartTime, EndTime). +check_add_with_limit([Spec, _MFA, {StartTime, EndTime}], {error, invalid_time, _}) -> + not is_valid_time(Spec, StartTime, EndTime). + +is_valid_time(Spec, {SH, SM, SS}, {EH, EM, ES}) -> + Start = SH * 3600 + SM * 60 + SS, + End = EH * 3600 + EM * 60 + ES, + case End > Start of + false -> + false; + true -> + case ecron_spec:parse_spec(Spec) of + {ok, cron, Job} -> + #{hour := HourSpec, minute := MinuteSpec, second := SecondSpec} = Job, + Hour = parse_max(23, HourSpec), + Minute = parse_max(59, MinuteSpec), + Second = parse_max(59, SecondSpec), + SpecTime = Hour * 3600 + Minute * 60 + Second, + SpecTime >= Start andalso SpecTime =< End; + _ -> true + end + end. -is_expired(Spec, StartTime, EndTime) -> - TZ = ecron:get_time_zone(), - {ok, Type, Job} = ecron_spec:parse_spec(Spec), - Start = ecron:datetime_to_millisecond(TZ, StartTime), - End = ecron:datetime_to_millisecond(TZ, EndTime), - [] =:= ecron:predict_datetime(activate, #{type => Type, crontab => Job}, Start, End, 10, TZ, erlang:system_time(millisecond)). +parse_max(Max, '*') -> Max; +parse_max(_DefaultMax, List) -> + case lists:last(List) of + {_, Max} -> Max; + Max -> Max + end. add_cron_new(Name, Spec, MFA) -> ecron:add(Name, Spec, MFA). add_cron_existing(Name, Spec, MFA) -> ecron:add(Name, Spec, MFA). -add_cron_new(Name, Spec, MFA, {Start, End}) -> ecron:add_with_datetime(Name, Spec, MFA, Start, End). -add_cron_existing(Name, Spec, MFA, {Start, End}) -> ecron:add_with_datetime(Name, Spec, MFA, Start, End). +add_cron_new(Name, Spec, MFA, {Start, End}) -> ecron:add_with_time(Name, Spec, MFA, Start, End). +add_cron_existing(Name, Spec, MFA, {Start, End}) -> ecron:add_with_time(Name, Spec, MFA, Start, End). add_with_count(Spec, MFA, Count) -> ecron:add_with_count(Spec, MFA, Count). -add_with_datetime(Spec, MFA, {Start, End}) -> ecron:add_with_datetime(Spec, MFA, Start, End). +add_with_datetime(Spec, MFA, {Start, End}) -> ecron:add_with_time(make_ref(), Spec, MFA, Start, End). add_every_new(Name, Ms, MFA) -> ecron:add(Name, Ms, MFA). add_every_existing(Name, Ms, MFA) -> ecron:add(Name, Ms, MFA). -add_every_new(Name, Ms, MFA, {Start, End}) -> ecron:add_with_datetime(Name, Ms, MFA, Start, End). -add_every_existing(Name, Ms, MFA, {Start, End}) -> ecron:add_with_datetime(Name, Ms, MFA, Start, End). +add_every_new(Name, Ms, MFA, {Start, End}) -> ecron:add_with_time(Name, Ms, MFA, Start, End). +add_every_existing(Name, Ms, MFA, {Start, End}) -> ecron:add_with_time(Name, Ms, MFA, Start, End). delete_existing(Name) -> ecron:delete(Name). delete_unknown(Name) -> ecron:delete(Name). @@ -242,13 +262,11 @@ valid_statistic(State, Name, {ok, Res}) -> length(Results) =:= length(RunMs) of true -> - Start = prop_ecron:to_ms(StartTime), - End = prop_ecron:to_ms(EndTime), case Type of cron -> - prop_ecron:check_cron_result(CrontabSpec, Start, End, Next); + prop_ecron:check_cron_result(CrontabSpec, StartTime, EndTime, Next); every -> - prop_ecron:check_every_result(CrontabSpec, second(Start), second(End), Next) + prop_ecron:check_every_result(CrontabSpec, StartTime, EndTime, local, Next) end; false -> false end @@ -257,9 +275,6 @@ valid_statistic(State, Name, {ok, Res}) -> in(Name, State) -> maps:is_key(Name, State). not_in(Name, State) -> not in(Name, State). -second(unlimited) -> unlimited; -second(Current) -> Current div 1000. - %%%%%%%%%%%%%%%%%% %%% Generators %%% %%%%%%%%%%%%%%%%%% @@ -273,15 +288,6 @@ mfa() -> {io_lib, format, ["~p", [range(1000, 2000)]]}. datetime() -> - {Year, _, _} = erlang:date(), - Start = - ?SUCHTHAT({DateS, _TimeS}, - {{range(Year - 100, Year), range(1, 12), range(1, 31)}, {range(0, 23), range(0, 59), range(0, 59)}}, - calendar:valid_date(DateS) - ), - End = - ?SUCHTHAT({DateS, _TimeS}, - {{range(Year + 1, Year + 100), range(1, 12), range(1, 31)}, {range(0, 23), range(0, 59), range(0, 59)}}, - calendar:valid_date(DateS) - ), + Start = {range(0, 12), range(0, 59), range(0, 59)}, + End = {range(13, 23), range(0, 59), range(0, 59)}, {Start, End}. diff --git a/test/prop_ecron_spec.erl b/test/prop_ecron_spec.erl index c6686c2..c1cd76a 100644 --- a/test/prop_ecron_spec.erl +++ b/test/prop_ecron_spec.erl @@ -3,7 +3,7 @@ -include_lib("proper/include/proper.hrl"). -export([datetime/0, month/0]). --export([standard_spec/0, extend_spec/0, integer_spec/2]). +-export([standard_spec/0, extend_spec/0, integer_spec/2, extend_spec/2]). -export([maybe_error_spec/0]). -export([crontab_spec/0]). @@ -36,6 +36,16 @@ extend_spec() -> oneof([integer_spec(0, 6), alphabet_spec(day_of_week, 0, 6)]) ]. +extend_spec(Min, Max) -> + [ + integer_spec(0, 59), + integer_spec(0, 59), + integer_spec(Min, Max), + integer_spec(1, 31), + oneof([integer_spec(1, 12), alphabet_spec(month, 1, 12)]), + oneof([integer_spec(0, 6), alphabet_spec(day_of_week, 0, 6)]) + ]. + maybe_error_spec() -> [ error_integer_spec(0, 100), %% second @@ -49,15 +59,15 @@ maybe_error_spec() -> integer_spec(Min, Max) -> frequency([ {1, "*"}, %% "*" - {1, {'*/Step', general, Min, Max, range(1, 100)}}, %% "*/Step" + {1, {'*/Step', general, Min, Max, range(1, 31)}}, %% "*/Step" {8, ?SIZED(S, integer_spec(S, Min, Max))}]). integer_spec(S, Min, Max) -> oneof([ {'Integer', general, range(Min, Max)}, %% 1 'Min-Max'(general, range(Min, Max), Max), %% 1-5 - 'Min-Max/Step'(general, range(Min, Max), Max, range(1, 100)), %% 1-5/2 - {'Min/Step', general, range(Min, Max), Max, range(1, 100)}, %% 10/2 + 'Min-Max/Step'(general, range(Min, Max), Max, range(1, 31)), %% 1-5/2 + {'Min/Step', general, range(Min, Max), Max, range(1, 31)}, %% 10/2 ?LAZY({list, general, [integer_spec(S - 1, Min, Max), integer_spec(S - 2, Min, Max)]}) %% 1,2-6/2... ]). @@ -68,7 +78,7 @@ error_integer_spec(S, Min, Max) -> oneof([ {'Integer', general, range(Min, Max)}, %% 1 'Min-Max'(general, range(Min, Max), Max), %% 1-5 - 'Min-Max/Step'(general, range(Min, Max), Max, range(1, 100)), %% 1-5/2 + 'Min-Max/Step'(general, range(Min, Max), Max, range(1, 31)), %% 1-5/2 ?LAZY({list, general, [error_integer_spec(S - 1, Min, Max), error_integer_spec(S - 2, Min, Max)]}) %% 1,2-6/2... ]). @@ -79,8 +89,8 @@ alphabet_spec(S, Type, Min, Max) -> oneof([ {'Integer', Type, range(Min, Max)}, %% 1 'Min-Max'(Type, range(Min, Max), Max), %% 1-5 - 'Min-Max/Step'(Type, range(Min, Max), Max, range(1, 100)), %% 1-5/2 - {'Min/Step', Type, range(Min, Max), Max, range(1, 100)}, %% 10/2 + 'Min-Max/Step'(Type, range(Min, Max), Max, range(1, 31)), %% 1-5/2 + {'Min/Step', Type, range(Min, Max), Max, range(1, 31)}, %% 10/2 ?LAZY({list, Type, [alphabet_spec(S - 1, Type, Min, Max), alphabet_spec(S - 2, Type, Min, Max)]}) %% 1,2-6/2... ]). diff --git a/test/prop_ecron_status.erl b/test/prop_ecron_status.erl index 3bd474f..a51b4ab 100644 --- a/test/prop_ecron_status.erl +++ b/test/prop_ecron_status.erl @@ -4,12 +4,8 @@ -export([prop_cron_apply_ok/0, prop_cron_apply_ok/1]). -export([prop_cron_apply_error/0, prop_cron_apply_error/1]). --export([prop_already_end/0, prop_already_end/1]). --export([prop_deactivate/0, prop_deactivate/1]). -export([prop_unknown/0, prop_unknown/1]). -export([prop_singleton/0, prop_singleton/1]). --export([prop_auto_remove/0, prop_auto_remove/1]). --export([prop_deactivate_already_ended/0, prop_deactivate_already_ended/1]). -export([prop_restart_server/0, prop_restart_server/1]). -export([prop_send_after/0, prop_send_after/1]). -export([prop_ecron_send_interval/0, prop_ecron_send_interval/1]). @@ -63,7 +59,7 @@ prop_cron_apply_error() -> {ok, Name} = ecron:add(Name, "@every 1s", MFA), timer:sleep(1100), {ok, #{crontab := CronSpec, - end_time := unlimited, start_time := unlimited, + start_time := {0, 0, 0}, end_time := {23, 59, 59}, mfa := RMFA, name := RName, failed := Failed, ok := Ok, results := Result}} = ecron:statistic(Name), @@ -77,47 +73,6 @@ prop_cron_apply_error() -> lists:all(fun(R) -> {element(1, R), element(2, R)} =:= {error, function_clause} end, Result) end). -prop_already_end(doc) -> "check already_end failed"; -prop_already_end(opts) -> [{numtests, 300}]. -prop_already_end() -> - ?FORALL({Name, Request, Shift}, {name(), term(), range(100, 1000)}, - begin - application:set_env(ecron, local_jobs, []), - application:ensure_all_started(ecron), - EndTime = reduce_time(Shift), - Name1 = {1, Name}, - Err1 = ecron:add_with_datetime(Name, "* * * * * *", {?MODULE, echo, [self(), Request]}, unlimited, EndTime), - Err2 = ecron:add_with_datetime(Name1, "@every 1s", {?MODULE, echo, [self(), Request]}, unlimited, EndTime), - Err3 = ecron:statistic(Name), - Err4 = ecron:statistic(Name1), - Err1 =:= {error, already_ended} andalso - Err2 =:= {error, already_ended} andalso - Err3 =:= {error, not_found} andalso - Err4 =:= {error, not_found} - end). - -prop_deactivate(doc) -> "deactive failed"; -prop_deactivate(opts) -> [{numtests, 10}]. -prop_deactivate() -> - ?FORALL({Name, Request, Shift}, {name(), term(), range(3, 4)}, - begin - application:set_env(ecron, local_jobs, []), - application:set_env(ecron, adjusting_time_second, 1), - application:ensure_all_started(ecron), - StartTime = add_time(Shift), - {ok, Name} = ecron:add_with_datetime(Name, "* * * * * *", {fun echo/2, [self(), Request]}, StartTime, unlimited), - {ok, #{status := Activate}} = ecron:statistic(Name), - ok = ecron:deactivate(Name), - {ok, #{status := Deactivate}} = ecron:statistic(Name), - ok = ecron:activate(Name), - timer:sleep(Shift * 1000 + 60), - Res = check_normal_response(Request, Shift, 2), - ok = ecron:delete(Name), - {error, not_found} = ecron:statistic(Name), - application:set_env(ecron, adjusting_time_second, 100000), - Deactivate =:= deactivate andalso Activate =:= activate andalso Res - end). - prop_unknown(doc) -> "unknown message"; prop_unknown(opts) -> [{numtests, 10}]. prop_unknown() -> @@ -172,7 +127,7 @@ prop_singleton() -> {ok, Name} = ecron:add(Name, "@every 1s", {timer, sleep, [1100]}, unlimited, unlimited, [{singleton, Singleton}]), timer:sleep(4200), {ok, Res} = ecron:statistic(Name), - #{start_time := unlimited, end_time := unlimited, status := activate, + #{start_time := {0, 0, 0}, end_time := {23, 59, 59}, status := activate, failed := 0, ok := Ok, results := Results, run_microsecond := RunMs } = Res, ecron:delete(Name), @@ -216,54 +171,19 @@ prop_ecron_send_interval() -> Res2 = receive Message -> ok after 1100 -> error end, Res3 = receive Message -> ok after 1100 -> error end, {ok, Res} = ecron:statistic(Job), - #{start_time := unlimited, end_time := unlimited, status := activate, + #{start_time := {0,0,0}, end_time := {23,59,59}, status := activate, failed := 0, ok := Ok, results := Results, run_microsecond := RunMs } = Res, erlang:send(Target, {exit, self()}), Res4 = receive exit -> ok after 800 -> error end, timer:sleep(160), {error, not_found} = ecron:statistic(Job), - {ok, Job1} = ecron:send_interval("0 1 1 * * *", Message, unlimited, unlimited, []), + {ok, Job1} = ecron:send_interval(ecron_local, make_ref(), "0 1 1 * * *", Message, unlimited, unlimited, []), error_logger:tty(true), Res1 =:= Res2 andalso Res2 =:= Res3 andalso Res1 =:= ok andalso Res4 =:= ok andalso length(Results) =:= Ok andalso length(RunMs) =:= Ok andalso Job1 =/= Job end). -prop_auto_remove(doc) -> "auto remove after already_ended"; -prop_auto_remove(opts) -> [{numtests, 5}]. -prop_auto_remove() -> - ?FORALL(Name, term(), - begin - application:ensure_all_started(ecron), - Shift = 2000, - EndMs = erlang:system_time(millisecond) + Shift, - EndTime = calendar:system_time_to_local_time(EndMs, millisecond), - {ok, Name} = ecron:add_with_datetime(Name, "@every 1s", {timer, sleep, [500]}, unlimited, EndTime), - timer:sleep(Shift + 100), - Result = ecron:statistic(Name), - Result =:= {error, not_found} - end). - -prop_deactivate_already_ended(doc) -> "auto remove after already_ended"; -prop_deactivate_already_ended(opts) -> [{numtests, 5}]. -prop_deactivate_already_ended() -> - ?FORALL(Name, term(), - begin - application:ensure_all_started(ecron), - Shift = 2000, - Now = erlang:system_time(millisecond), - EndMs = Now + Shift, - EndTime = calendar:system_time_to_local_time(EndMs, millisecond), - StartMs = Now - Shift, - StartTime = calendar:system_time_to_local_time(StartMs, millisecond), - {error, invalid_time, {EndTime, StartTime}} = ecron:add_with_datetime("@every 1s", {timer, sleep, [500]}, EndTime, StartTime), - {ok, Name} = ecron:add_with_datetime(Name, "@every 1s", {timer, sleep, [500]}, unlimited, EndTime), - ok = ecron:deactivate(Name), - timer:sleep(Shift + 100), - Result = ecron:activate(Name), - Result =:= {error, already_ended} - end). - prop_add_with_count(doc) -> "add_with_count"; prop_add_with_count(opts) -> [{numtests, 5}]. prop_add_with_count() -> @@ -300,16 +220,6 @@ check_normal_response(Msg, Number, Ms, LastMs) -> now_millisecond() -> erlang:system_time(millisecond). -reduce_time(Shift) -> - Now = calendar:local_time(), - calendar:gregorian_seconds_to_datetime( - calendar:datetime_to_gregorian_seconds(Now) - Shift). - -add_time(Shift) -> - Now = calendar:local_time(), - calendar:gregorian_seconds_to_datetime( - calendar:datetime_to_gregorian_seconds(Now) + Shift). - store() -> receive {exit, Pid} -> erlang:send(Pid, exit); From 48280fc301fcf2a972e89e9b667238da4d533720 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 5 Mar 2021 15:50:09 +0800 Subject: [PATCH 06/11] update README example --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ src/ecron.erl | 9 +++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b386a8..a9d204f 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,45 @@ You can find a collection of general practices in [Full Erlang Examples](https:/ You can also reload task manually by `ecron:reload().` when the system time is manually modified. * Global jobs depend on [global](http://erlang.org/doc/man/global.html), only allowed to be added statically, [check this for more detail](https://github.com/zhongwencool/ecron/blob/master/doc/global.md). +## Supervisor Tree Usage +Ecron starts with a standalone process(`ecron_local`) to manage all jobs by default, +but you can also set up your own job management process for each application, +which has the advantage that you can precisely control its start time and stop timing. +This has the advantage that you can precisely control when it starts and when it stops. +The Jobs between various applications do not affect each other. + +Ecron must be included in your application's supervision tree. +All of your configuration is passed into the supervisor: +```erlang +%%config/sys.config +[{your_app, [{crontab_jobs, [ + {crontab_job, "*/15 * * * *", {stateless_cron, inspect, ["Runs on 0, 15, 30, 45 minutes"]}} + ]} +]. + +%% src/your_app_sup.erl +-module(your_app_top_sup). +-behaviour(supervisor). +-export([init/1]). + +init(_Args) -> + Jobs = application:get_env(your_app, crontab_jobs, []), + SupFlags = #{ + strategy => one_for_one, + intensity => 100, + period => 30 + }, + Name = 'uniqueName', + CronSpec = #{ + id => Name, + start => {ecron, start_link, [{local, Name}, Jobs]}, + restart => permanent, + shutdown => 1000, + type => worker + }, + {ok, {SupFlags, [CronSpec]}}. + +``` ## Advanced Usage ```erlang diff --git a/src/ecron.erl b/src/ecron.erl index de79b05..4348cbe 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -258,8 +258,13 @@ activate(JobName) -> activate(?LocalJob, JobName). activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, infinity). %% @equiv statistic(ecron_local,Name). --spec statistic(name()) -> {ok, statistic()} | {error, not_found}. -statistic(JobName) -> statistic(?LocalJob, JobName). +-spec statistic(register()) -> {ok, statistic()} | {error, not_found}. +statistic(Register) -> + case ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) of + [] -> {error, not_found}; + List -> {ok, List} + end. + %% @doc %% Statistic from an exist job. %% if the job is nonexistent, return `{error, not_found}'. From 8892055ae7c5239e852db681d660bb679efdb59f Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 5 Mar 2021 16:07:06 +0800 Subject: [PATCH 07/11] update stat/1 --- src/ecron.erl | 33 +++++++++++++++++++-------------- src/ecron_spec.erl | 3 ++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ecron.erl b/src/ecron.erl index 4348cbe..18cbf92 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -41,7 +41,7 @@ %%%=================================================================== %%% API %%%=================================================================== --type register() ::atom(). +-type register() :: atom(). -type name() :: term(). -type crontab_spec() :: crontab() | string() | binary() | 1..4294967. @@ -87,8 +87,7 @@ -type options() :: [option()]. %% @equiv add(ecron_local, JobName, Spec, MFA) --spec add(name(), crontab_spec(), mfargs()) -> - {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. +-spec add(name(), crontab_spec(), mfargs()) -> {ok, name()} | {error, parse_error(), term()} | {error, already_exist}. add(JobName, Spec, MFA) -> add(?LocalJob, JobName, Spec, MFA). @@ -99,8 +98,7 @@ add(Register, JobName, Spec, MFA) -> add(Register, JobName, Spec, MFA, unlimited, unlimited, []). %% @equiv add_with_count(ecron_local, make_ref(), Spec, MFA, RunCount) --spec add_with_count(crontab_spec(), mfargs(), pos_integer()) -> - {ok, name()} | {error, parse_error(), term()}. +-spec add_with_count(crontab_spec(), mfargs(), pos_integer()) -> {ok, name()} | {error, parse_error(), term()}. add_with_count(Spec, MFA, RunCount) when is_integer(RunCount) -> add_with_count(?LocalJob, make_ref(), Spec, MFA, RunCount). @@ -198,8 +196,7 @@ send_interval(Spec, Pid, Message) -> send_interval(?LocalJob, make_ref(), Spec, Pid, Message). %% @equiv send_interval(ecron_local,name(), Spec, Pid, Message, unlimited, unlimited, []) --spec send_interval(register(), name(), crontab_spec(), pid(), term()) -> - {ok, name()} | {error, parse_error(), term()}. +-spec send_interval(register(), name(), crontab_spec(), pid(), term()) -> {ok, name()} | {error, parse_error(), term()}. send_interval(Register, Name, Spec, Pid, Message) -> send_interval(Register, Name, Spec, Pid, Message, unlimited, unlimited, []). @@ -241,6 +238,7 @@ delete(Register, JobName) -> gen_server:call(Register, {delete, JobName}, infini %% @equiv deactivate(ecron_local, Name). -spec deactivate(name()) -> ok | {error, not_found}. deactivate(JobName) -> deactivate(?LocalJob, JobName). + %% @doc %% Deactivate an exist job, if the job is nonexistent, return `{error, not_found}'. %% just freeze the job, use @see activate/2 to unfreeze job. @@ -250,6 +248,7 @@ deactivate(Register, JobName) -> gen_server:call(Register, {deactivate, JobName} %% @equiv activate(ecron_local, Name). -spec activate(name()) -> ok | {error, not_found}. activate(JobName) -> activate(?LocalJob, JobName). + %% @doc %% Activate an exist job, if the job is nonexistent, return `{error, not_found}'. %% if the job is already activate, nothing happened. @@ -258,13 +257,18 @@ activate(JobName) -> activate(?LocalJob, JobName). activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, infinity). %% @equiv statistic(ecron_local,Name). --spec statistic(register()) -> {ok, statistic()} | {error, not_found}. +-spec statistic(register() | name()) -> {ok, statistic()} | {error, not_found}. statistic(Register) -> - case ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) of - [] -> {error, not_found}; - List -> {ok, List} + case erlang:whereis(Register) of + undefined -> + statistic(?LocalJob, Register); + _ -> + case ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) of + [] -> {error, not_found}; + List -> {ok, List} + end end. - + %% @doc %% Statistic from an exist job. %% if the job is nonexistent, return `{error, not_found}'. @@ -521,8 +525,9 @@ next_schedule_millisecond(cron, Spec, TimeZone, Now, Start, End) -> next_schedule_millisecond2(Spec, MinSpec, ForwardDateTime, Start, End, TimeZone). next_schedule_millisecond2(Spec, MinSpec, ForwardDateTime, Start, End, TimeZone) -> - NextDateTime = {_, {NH, NM, NS}} - = next_schedule_datetime(Spec, MinSpec, ForwardDateTime, Start, End), + NextDateTime = + {_, {NH, NM, NS}} = + next_schedule_datetime(Spec, MinSpec, ForwardDateTime, Start, End), Next = NH * 3600 + NM * 60 + NS, {SHour, SMin, SSec} = Start, {EHour, EMin, ESec} = End, diff --git a/src/ecron_spec.erl b/src/ecron_spec.erl index e8ae64f..b70ea8e 100644 --- a/src/ecron_spec.erl +++ b/src/ecron_spec.erl @@ -29,7 +29,8 @@ valid_time(cron, {SH, SM, SS}, {EH, EM, ES}, CronTab) -> valid_time(every, {SH, SM, SS}, {EH, EM, ES}, _CronTab) -> ((EH - SH) * 3600 + (EM - SM) * 60 + (ES - SS)) >= 0. -parse_max(Max, '*') -> Max; +parse_max(Max, '*') -> + Max; parse_max(_DefaultMax, List) -> case lists:last(List) of {_, Max} -> Max; From 800b5f3377bc4386091f9e90b353030fa5a55029 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 9 Mar 2021 09:11:48 +0800 Subject: [PATCH 08/11] fixed statistic/2 test failed --- .../apps/titan/lib/titan/stateless_cron.ex | 26 ++++----- examples/titan_elixir/config/config.exs | 2 +- .../apps/titan/src/stateless_cron.erl | 36 ++++++------- .../titan_erlang/apps/titan/src/titan_sup.erl | 16 +++++- examples/titan_erlang/config/sys.config | 53 ++++++++++--------- examples/titan_erlang/rebar.lock | 7 ++- src/ecron.erl | 8 +-- 7 files changed, 82 insertions(+), 66 deletions(-) diff --git a/examples/titan_elixir/apps/titan/lib/titan/stateless_cron.ex b/examples/titan_elixir/apps/titan/lib/titan/stateless_cron.ex index 0b5e88a..19fbe32 100644 --- a/examples/titan_elixir/apps/titan/lib/titan/stateless_cron.ex +++ b/examples/titan_elixir/apps/titan/lib/titan/stateless_cron.ex @@ -10,24 +10,24 @@ defmodule StatelessCron do end ## Add job run at At minute 23 past every 2nd hour from 0 through 20 - ## between {{2020, 1, 1}, {0, 0, 0}} and {{2022, 1, 1}, {0, 0, 0}} - ## by ecron:add_with_datetime / 4 - def add_limited_start_end_datetime_job2() do + ## between {12, 0, 0} and {18, 0, 0} + ## by ecron:add_with_datetime / 5 + def add_limited_start_end_time_job2() do mfa = {__MODULE__, :inspect, ["at minute 23 past every 2nd hour from 0 through 20."]} - start_time = {{2020, 1, 1}, {0, 0, 0}} #datetime or `unlimited` - end_time = {{2022, 1, 1}, {0, 0, 0}} # datetime or `unlimited` - :ecron.add_with_datetime("0 23 0-20/2 * * *", mfa, start_time, end_time) + start_time = {12, 0, 0} #datetime or `unlimited` + end_time = {18, 0, 0} # datetime or `unlimited` + :ecron.add_with_time(make_ref(), "0 23 0-20/2 * * *", mfa, start_time, end_time) end ## Add job run at 14:15 on day-of-month 1 - ##between {{2020, 1, 1}, {0, 0, 0}} and {{2022, 1, 1}, {0, 0, 0}} + ##between {{11, 0, 0}} and {{22, 10, 0}} ## by ecron:add_with_datetime / 5 - def add_limited_start_end_datetime_job() do + def add_limited_start_end_time_job() do job_name = :limited_start_end_datetime_cron_job - mfa = {__MODULE__, :inspect, ["at 14:15 on day-of-month 1."]} - start_time = {{2020, 1, 1}, {0, 0, 0}} # datetime or `unlimited` - end_time = {{2022, 1, 1}, {0, 0, 0}} # datetime or `unlimited` - {:ok, ^job_name} = :ecron.add_with_datetime(job_name, "0 15 14 1 * *", mfa, start_time, end_time) + mfa = {__MODULE__, :inspect, ["at 14~23:15 on day-of-month 1."]} + start_time = {11, 0, 0} # time or `unlimited` + end_time = {22, 10, 0} # time or `unlimited` + {:ok, ^job_name} = :ecron.add_with_time(job_name, "0 15 14-23 1 * *", mfa, start_time, end_time) end ## Can only run 100 times at 22:00 on every day-of-week from Monday through Friday. @@ -42,7 +42,7 @@ defmodule StatelessCron do def add_limited_run_count_job2() do mfa = {__MODULE__, :inspect, ["at minute 0 past hour 0 and 12 on day-of-month 1 in every 2nd month."]} job_name = :limited_run_count_cron_job - :ecron.add_with_count(job_name, "0 22 * * 1-5", mfa, 100) + :ecron.add_with_count(:ecron_job, job_name, "0 22 * * 1-5", mfa, 100) end ## Delete a specific task diff --git a/examples/titan_elixir/config/config.exs b/examples/titan_elixir/config/config.exs index 1eda0e3..a11c864 100644 --- a/examples/titan_elixir/config/config.exs +++ b/examples/titan_elixir/config/config.exs @@ -43,7 +43,7 @@ config :ecron, :local_jobs, :limit_datetime_job, "@hourly", {StatelessCron, :inspect, ["Runs every(0-23) o'clock"]}, - {{2019, 9, 26}, {0, 0, 0}}, + {11, 0, 0}, :unlimited }, # Job with max run count, default is `unlimited` diff --git a/examples/titan_erlang/apps/titan/src/stateless_cron.erl b/examples/titan_erlang/apps/titan/src/stateless_cron.erl index 050fae9..bcc68d4 100644 --- a/examples/titan_erlang/apps/titan/src/stateless_cron.erl +++ b/examples/titan_erlang/apps/titan/src/stateless_cron.erl @@ -2,8 +2,8 @@ %% API -export([add_every_4am_job/0]). --export([add_limited_start_end_datetime_job/0]). --export([add_limited_start_end_datetime_job2/0]). +-export([add_limited_start_end_time_job/0]). +-export([add_limited_start_end_time_job2/0]). -export([add_limited_run_count_job/0]). -export([add_limited_run_count_job2/0]). -export([delete_job/1]). @@ -20,23 +20,23 @@ add_every_4am_job() -> {ok, JobName} = ecron:add(JobName, "0 4 * * *", MFA). %% Add job run at At minute 23 past every 2nd hour from 0 through 20 -%% between {{2020, 1, 1}, {0, 0, 0}} and {{2022, 1, 1}, {0, 0, 0}} -%% by ecron:add_with_datetime/4 -add_limited_start_end_datetime_job2() -> +%% between {10, 0, 0} and {23, 59, 59} +%% by ecron:add_with_time/5 +add_limited_start_end_time_job2() -> MFA = {?MODULE, inspect, ["at minute 23 past every 2nd hour from 0 through 20."]}, - Start = {{2020, 1, 1}, {0, 0, 0}}, %% datetime or `unlimited` - End = {{2022, 1, 1}, {0, 0, 0}}, %% datetime or `unlimited` - ecron:add_with_datetime("0 23 0-20/2 * * *", MFA, Start, End). - -%% Add job run at 14:15 on day-of-month 1 -%% between {{2020, 1, 1}, {0, 0, 0}} and {{2022, 1, 1}, {0, 0, 0}} -%% by ecron:add_with_datetime/5 -add_limited_start_end_datetime_job() -> + Start = {{10, 0, 0}}, %% time or `unlimited` + End = unlimited, %% time or `unlimited` + ecron:add_with_time(make_ref(), "0 23 0-20/2 * * *", MFA, Start, End). + +%% Add job run at 14~23:00-15-30-45 on day-of-month 1 +%% between {10, 0, 0} and {22, 0, 0} +%% by ecron:add_with_time/5 +add_limited_start_end_time_job() -> JobName = limited_start_end_datetime_cron_job, - MFA = {?MODULE, inspect, ["at 14:15 on day-of-month 1."]}, - Start = {{2020, 1, 1}, {0, 0, 0}}, %% datetime or `unlimited` - End = {{2022, 1, 1}, {0, 0, 0}}, %% datetime or `unlimited` - {ok, JobName} = ecron:add_with_datetime(JobName, "0 15 14 1 * *", MFA, Start, End). + MFA = {?MODULE, inspect, ["at 14~23:00-15-30-45 on day-of-month 1."]}, + Start = {10, 0, 0}, %% time or `unlimited` + End = {22, 15, 0}, %% time or `unlimited` + {ok, JobName} = ecron:add_with_time(JobName, "0 */15 14-23 1 * *", MFA, Start, End). %% Can only run 100 times at 22:00 on every day-of-week from Monday through Friday. %% by ecron:add_with_count/3 @@ -49,7 +49,7 @@ add_limited_run_count_job() -> add_limited_run_count_job2() -> MFA = {?MODULE, inspect, ["at minute 0 past hour 0 and 12 on day-of-month 1 in every 2nd month."]}, JobName = limited_run_count_cron_job, - ecron:add_with_count(JobName, "0 22 * * 1-5", MFA, 100). + ecron:add_with_count(ecron_local, JobName, "0 22 * * 1-5", MFA, 100). %% Delete a specific task delete_job(JobName) -> diff --git a/examples/titan_erlang/apps/titan/src/titan_sup.erl b/examples/titan_erlang/apps/titan/src/titan_sup.erl index cb99f3a..1343d02 100644 --- a/examples/titan_erlang/apps/titan/src/titan_sup.erl +++ b/examples/titan_erlang/apps/titan/src/titan_sup.erl @@ -18,14 +18,26 @@ init([]) -> intensity => 100, period => 10 }, + CrontabName = titan_cron, + Jobs = application:get_env(titan, crontab_jobs, []), + Crontab = + #{ + id => CrontabName, + start => {ecron, start_link, [{local, CrontabName}, Jobs]}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [CrontabName] + }, WorkMods = [stateful_cron_by_send_after, stateful_cron_by_send_interval], ChildSpecs = [begin - #{id => Mod, + #{ + id => Mod, start => {Mod, start_link, []}, restart => permanent, shutdown => 1000, type => worker, modules => [Mod] } end || Mod <- WorkMods], - {ok, {SupFlags, ChildSpecs}}. + {ok, {SupFlags, [Crontab|ChildSpecs]}}. diff --git a/examples/titan_erlang/config/sys.config b/examples/titan_erlang/config/sys.config index cca2967..062dda5 100644 --- a/examples/titan_erlang/config/sys.config +++ b/examples/titan_erlang/config/sys.config @@ -1,35 +1,36 @@ [ - {titan, []}, + {titan, [ + {crontab_jobs, [ + %% {JobName, CrontabSpec, {M, F, A}} + %% {JobName, CrontabSpec, {M, F, A}, StartDateTime, EndDateTime} + %% CrontabSpec + %% 1. "Minute Hour DayOfMonth Month DayOfWeek" + %% 2. "Second Minute Hour DayOfMonth Month DayOfWeek" + %% 3. @yearly | @annually | @monthly | @weekly | @daily | @midnight | @hourly | @minutely + %% 4. @every 1h2m3s + + %% Standard crontab spec without second (default second is 0 not *). + {crontab_job, "*/15 * * * *", {stateless_cron, inspect, ["Runs on 0, 15, 30, 45 minutes"]}}, + %% Extend crontab spec with second. + {extend_crontab_job, "0 0 1-6/2,18 * * *", {stateless_cron, inspect, ["Runs on 1,3,6,18 o'clock"]}}, + %% Crontab spec with alphabet. + {alphabet_job, "@hourly", {stateless_cron, inspect, ["Runs every(0-23) o'clock"]}}, + %% Fixed interval spec. + {fixed_interval_job, "@every 5m", {stateless_cron, inspect, ["Runs every 5 minutes"]}}, + %% Job with startDateTime and EndDateTime. Runs 0-23 o'clock since {11,0,0}. + {limit_datetime_job, "@hourly", {stateless_cron, inspect, ["Runs every(0-23) o'clock"]}, {11, 0, 0}, unlimited}, + %% Job with max run count, default is `unlimited` + {max_run_count_job, "@daily", {stateless_cron, inspect, ["Runs daily"]}, unlimited, unlimited, [{max_count, 1000}]}, + %% Parallel job, singleton default is true. + {no_singleton_job, "@minutely", {timer, sleep, [61000]}, unlimited, unlimited, [{singleton, false}]} + + ]} + ]}, {ecron, [ {time_zone, local}, %% local or utc {global_quorum_size, 1}, {global_jobs, [ {global_crontab_job, "*/15 * * * * *", {stateless_cron, inspect, ["Runs on 0, 15, 30, 45 seconds"]}} - ]}, - {local_jobs, [ - %% {JobName, CrontabSpec, {M, F, A}} - %% {JobName, CrontabSpec, {M, F, A}, StartDateTime, EndDateTime} - %% CrontabSpec - %% 1. "Minute Hour DayOfMonth Month DayOfWeek" - %% 2. "Second Minute Hour DayOfMonth Month DayOfWeek" - %% 3. @yearly | @annually | @monthly | @weekly | @daily | @midnight | @hourly | @minutely - %% 4. @every 1h2m3s - - %% Standard crontab spec without second (default second is 0 not *). - {crontab_job, "*/15 * * * *", {stateless_cron, inspect, ["Runs on 0, 15, 30, 45 minutes"]}}, - %% Extend crontab spec with second. - {extend_crontab_job, "0 0 1-6/2,18 * * *", {stateless_cron, inspect, ["Runs on 1,3,6,18 o'clock"]}}, - %% Crontab spec with alphabet. - {alphabet_job, "@hourly", {stateless_cron, inspect, ["Runs every(0-23) o'clock"]}}, - %% Fixed interval spec. - {fixed_interval_job, "@every 5m", {stateless_cron, inspect, ["Runs every 5 minutes"]}}, - %% Job with startDateTime and EndDateTime. Runs 0-23 o'clock since {{2019,9,26},{0,0,0}}. - {limit_datetime_job, "@hourly", {stateless_cron, inspect, ["Runs every(0-23) o'clock"]}, {{2019, 9, 26}, {0, 0, 0}}, unlimited}, - %% Job with max run count, default is `unlimited` - {max_run_count_job, "@daily", {stateless_cron, inspect, ["Runs daily"]}, unlimited, unlimited, [{max_count, 1000}]}, - %% Parallel job, singleton default is true. - {no_singleton_job, "@minutely", {timer, sleep, [61000]}, unlimited, unlimited, [{singleton, false}]} - ]} ]} ]. diff --git a/examples/titan_erlang/rebar.lock b/examples/titan_erlang/rebar.lock index a9bf721..a7ba966 100644 --- a/examples/titan_erlang/rebar.lock +++ b/examples/titan_erlang/rebar.lock @@ -1,8 +1,11 @@ -{"1.1.0", +{"1.2.0", [{<<"ecron">>,{pkg,<<"ecron">>,<<"0.5.2">>},0}, {<<"telemetry">>,{pkg,<<"telemetry">>,<<"0.4.0">>},1}]}. [ {pkg_hash,[ {<<"ecron">>, <<"66E092F1E41C6C52DE7C0C88F023ECC0DBE44FBB614BD9BC8ABA38BA5B6DEFF0">>}, - {<<"telemetry">>, <<"8339BEE3FA8B91CB84D14C2935F8ECF399CCD87301AD6DA6B71C09553834B2AB">>}]} + {<<"telemetry">>, <<"8339BEE3FA8B91CB84D14C2935F8ECF399CCD87301AD6DA6B71C09553834B2AB">>}]}, +{pkg_hash_ext,[ + {<<"ecron">>, <<"6FEB15514E9BD60B7696AABA9C405AEA8F950C8CA1D8E1FD4C702A12073F30C6">>}, + {<<"telemetry">>, <<"E9E3CACFD37C1531C0CA70CA7C0C30CE2DBB02998A4F7719DE180FE63F8D41E4">>}]} ]. diff --git a/src/ecron.erl b/src/ecron.erl index 18cbf92..9767acf 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -132,7 +132,7 @@ add(JobName, Spec, MFA, Start, End, Opts) -> %%
  • `JobName': The unique name of job, return `{error, already_exist}' if JobName is already exist.
  • %%
  • `Spec': A cron expression represents a set of times.
  • %%
  • `MFA': Spawn a process to run MFA when crontab is triggered.
  • -%%
  • `Start': The job's next trigger time is Calculated from StartDatetime. Keeping `unlimited' if start from now on.
  • +%%
  • `Start': The job's next trigger time is Calculated from StartTime. Keeping `unlimited' if start from now on.
  • %%
  • `End': The job will be remove at end time. Keeping `unlimited' if never end.
  • %%
  • `Opts': The optional list of options. `{singleton, true}': Default job is singleton, Each task cannot be executed concurrently. %% `{max_count, pos_integer()}': This task can be run up to `MaxCount' times, default is `unlimited'. @@ -259,10 +259,10 @@ activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, in %% @equiv statistic(ecron_local,Name). -spec statistic(register() | name()) -> {ok, statistic()} | {error, not_found}. statistic(Register) -> - case erlang:whereis(Register) of - undefined -> + case is_atom(Register) andalso undefined =/= erlang:whereis(Register) of + true -> statistic(?LocalJob, Register); - _ -> + false -> case ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) of [] -> {error, not_found}; List -> {ok, List} From 435563e90458511dfb43bdecd382d038c2b6cc21 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 9 Mar 2021 09:53:15 +0800 Subject: [PATCH 09/11] update stat --- src/ecron.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ecron.erl b/src/ecron.erl index 9767acf..3e2607d 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -260,9 +260,9 @@ activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, in -spec statistic(register() | name()) -> {ok, statistic()} | {error, not_found}. statistic(Register) -> case is_atom(Register) andalso undefined =/= erlang:whereis(Register) of - true -> - statistic(?LocalJob, Register); false -> + statistic(?LocalJob, Register); + true -> case ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) of [] -> {error, not_found}; List -> {ok, List} From 067db7beb9224302920a656ea9534a504b622de9 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 9 Mar 2021 13:56:55 +0800 Subject: [PATCH 10/11] fixed statistic/1 --- src/ecron.erl | 15 ++++++++------- test/prop_ecron_basic_SUITE.erl | 3 +++ test/prop_ecron_global_SUITE.erl | 4 ++-- test/prop_ecron_server.erl | 4 ++-- test/prop_ecron_status.erl | 12 ++++++------ 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/ecron.erl b/src/ecron.erl index 3e2607d..8c3c97c 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -256,17 +256,18 @@ activate(JobName) -> activate(?LocalJob, JobName). -spec activate(register(), name()) -> ok | {error, not_found}. activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, infinity). -%% @equiv statistic(ecron_local,Name). --spec statistic(register() | name()) -> {ok, statistic()} | {error, not_found}. +-spec statistic(register() | name()) -> [statistic()]. statistic(Register) -> case is_atom(Register) andalso undefined =/= erlang:whereis(Register) of false -> - statistic(?LocalJob, Register); + case ets:lookup(?LocalJob, Register) of + [Job] -> + job_to_statistic(Job); + [] -> + [] + end; true -> - case ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) of - [] -> {error, not_found}; - List -> {ok, List} - end + ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) end. %% @doc diff --git a/test/prop_ecron_basic_SUITE.erl b/test/prop_ecron_basic_SUITE.erl index e3fe33e..0926b58 100644 --- a/test/prop_ecron_basic_SUITE.erl +++ b/test/prop_ecron_basic_SUITE.erl @@ -37,6 +37,9 @@ basic(_Config) -> MFA = {io,format,["Mail checking~n"]}, {ok, JobName} = ecron:add(?NAME, JobName, "0 0 8 * * 1-5", MFA, unlimited, unlimited, []), {ok, Result} = ecron:statistic(?NAME, JobName), + [Result1] = ecron:statistic(?NAME), + [] = ecron:statistic(ecron_not_found), + ?assertEqual(Result1, Result, "ecron:statistic/2"), #{crontab := #{day_of_month := '*', day_of_week := [{1,5}], diff --git a/test/prop_ecron_global_SUITE.erl b/test/prop_ecron_global_SUITE.erl index 80fa4d0..f83726d 100644 --- a/test/prop_ecron_global_SUITE.erl +++ b/test/prop_ecron_global_SUITE.erl @@ -122,12 +122,12 @@ transfer(_Config) -> Pid1 = global:whereis_name(?GlobalJob), true = is_pid(Pid1), start_master(1), - {ok, #{node := Node1}} = ecron:statistic(global_job), + {ok, #{node := Node1}} = ecron:statistic(ecron_local, global_job), ok = rpc:call(Node1, application, stop, [ecron]), Pid2 = global:whereis_name(?GlobalJob), true = is_pid(Pid2), true = (Pid2 =/= Pid1), - {error, not_found} = ecron:statistic(no_found), + {error, not_found} = ecron:statistic(ecron_local, no_found), [#{node := Node2}] = ecron:statistic(), true = Node1 =/= Node2, stop_slave(?Slave1), diff --git a/test/prop_ecron_server.erl b/test/prop_ecron_server.erl index 55fe7c1..862427a 100644 --- a/test/prop_ecron_server.erl +++ b/test/prop_ecron_server.erl @@ -124,7 +124,7 @@ postcondition(_State, {call, _Mod, activate_unknown, [_Name | _]}, Res) -> postcondition(_State, {call, _Mod, activate_existing, [_Name | _]}, Res) -> Res =:= ok; postcondition(_State, {call, _Mod, statistic_unknown, [_Name | _]}, Res) -> - Res =:= {error, not_found}; + Res =:= []; postcondition(State, {call, _Mod, statistic_existing, [Name | _]}, Res) -> valid_statistic(State, Name, Res); postcondition(_State, {call, _Mod, reload, []}, Res) -> @@ -246,7 +246,7 @@ statistic_existing(Name) -> ecron:statistic(Name). statistic_all() -> ecron:statistic(). reload() -> ecron:reload(). -valid_statistic(State, Name, {ok, Res}) -> +valid_statistic(State, Name, [Res]) -> case maps:find(Name, State) of error -> false; {ok, #{cron := #{type := Type, crontab := CrontabSpec, mfa := MFAExpect}}} -> diff --git a/test/prop_ecron_status.erl b/test/prop_ecron_status.erl index a51b4ab..396b1d9 100644 --- a/test/prop_ecron_status.erl +++ b/test/prop_ecron_status.erl @@ -62,7 +62,7 @@ prop_cron_apply_error() -> start_time := {0, 0, 0}, end_time := {23, 59, 59}, mfa := RMFA, name := RName, failed := Failed, ok := Ok, - results := Result}} = ecron:statistic(Name), + results := Result}} = ecron:statistic(ecron_local, Name), ok = ecron:delete(Name), error_logger:tty(true), CronSpec =:= 1 andalso @@ -100,12 +100,12 @@ prop_restart_server() -> ]), application:ensure_all_started(ecron), {ok, Name} = ecron:add(Name, "@yearly", {io, format, ["Yearly~n"]}), - Res1 = ecron:statistic(Name), + Res1 = ecron:statistic(ecron_local, Name), Pid = erlang:whereis(?LocalJob), erlang:exit(Pid, kill), timer:sleep(200), NewPid = erlang:whereis(?LocalJob), - Res2 = ecron:statistic(Name), + Res2 = ecron:statistic(ecron_local, Name), ok = ecron:delete(Name), error_logger:tty(true), Pid =/= NewPid andalso @@ -126,7 +126,7 @@ prop_singleton() -> application:start(ecron), {ok, Name} = ecron:add(Name, "@every 1s", {timer, sleep, [1100]}, unlimited, unlimited, [{singleton, Singleton}]), timer:sleep(4200), - {ok, Res} = ecron:statistic(Name), + {ok, Res} = ecron:statistic(ecron_local, Name), #{start_time := {0, 0, 0}, end_time := {23, 59, 59}, status := activate, failed := 0, ok := Ok, results := Results, run_microsecond := RunMs } = Res, @@ -170,14 +170,14 @@ prop_ecron_send_interval() -> Res1 = receive Message -> ok after 1100 -> error end, Res2 = receive Message -> ok after 1100 -> error end, Res3 = receive Message -> ok after 1100 -> error end, - {ok, Res} = ecron:statistic(Job), + {ok, Res} = ecron:statistic(ecron_local, Job), #{start_time := {0,0,0}, end_time := {23,59,59}, status := activate, failed := 0, ok := Ok, results := Results, run_microsecond := RunMs } = Res, erlang:send(Target, {exit, self()}), Res4 = receive exit -> ok after 800 -> error end, timer:sleep(160), - {error, not_found} = ecron:statistic(Job), + {error, not_found} = ecron:statistic(ecron_local, Job), {ok, Job1} = ecron:send_interval(ecron_local, make_ref(), "0 1 1 * * *", Message, unlimited, unlimited, []), error_logger:tty(true), Res1 =:= Res2 andalso Res2 =:= Res3 andalso Res1 =:= ok andalso Res4 =:= ok andalso From da30336aa3279759d40beac1d354ce61b8e3a5ef Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 9 Mar 2021 14:31:54 +0800 Subject: [PATCH 11/11] ecron:start_link support {local, Name} and Name --- README.md | 2 +- src/ecron.erl | 12 +++++------- test/prop_ecron_basic_SUITE.erl | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a9d204f..df2bb5d 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ init(_Args) -> Name = 'uniqueName', CronSpec = #{ id => Name, - start => {ecron, start_link, [{local, Name}, Jobs]}, + start => {ecron, start_link, [Name, Jobs]}, restart => permanent, shutdown => 1000, type => worker diff --git a/src/ecron.erl b/src/ecron.erl index 8c3c97c..8f2629c 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -260,12 +260,7 @@ activate(Register, JobName) -> gen_server:call(Register, {activate, JobName}, in statistic(Register) -> case is_atom(Register) andalso undefined =/= erlang:whereis(Register) of false -> - case ets:lookup(?LocalJob, Register) of - [Job] -> - job_to_statistic(Job); - [] -> - [] - end; + [job_to_statistic(Job) || Job <- ets:lookup(?LocalJob, Register)]; true -> ets:foldl(fun(Job, Acc) -> [job_to_statistic(Job) | Acc] end, [], Register) end. @@ -335,6 +330,7 @@ predict_datetime(Job, Num) -> %%%=================================================================== %%% CallBack %%%=================================================================== + start_link({_, JobTab} = Name, JobSpec) -> case ecron_spec:parse_crontab(JobSpec, []) of {ok, Jobs} -> @@ -342,7 +338,9 @@ start_link({_, JobTab} = Name, JobSpec) -> gen_server:start_link(Name, ?MODULE, [JobTab, Jobs], []); {error, Reason} -> {error, Reason} - end. + end; +start_link(JobTab, JobSpec) when is_atom(JobTab) -> + start_link({local, JobTab}, JobSpec). init([JobTab, Jobs]) -> erlang:process_flag(trap_exit, true), diff --git a/test/prop_ecron_basic_SUITE.erl b/test/prop_ecron_basic_SUITE.erl index 0926b58..98c098c 100644 --- a/test/prop_ecron_basic_SUITE.erl +++ b/test/prop_ecron_basic_SUITE.erl @@ -29,7 +29,7 @@ end_per_suite(_Config) -> init_per_testcase(_TestCase, Config) -> application:ensure_all_started(ecron), - {ok, _Pid} = ecron:start_link({local, ?NAME}, []), + {ok, _Pid} = ecron:start_link(?NAME, []), Config. basic(_Config) ->