From aecd0e7debba992fc490c0375e06dd822f2be28c Mon Sep 17 00:00:00 2001 From: Zach Goldberg Date: Wed, 11 Feb 2015 14:22:45 -0800 Subject: [PATCH 1/2] Support two types of cron: interval and fixed This updates mod_cron to have much more unix-cron like behavior. The old version would start an interval timer whenever ejabberd started, which isn't a particularly useful starting point (I don't care for my regular activities to be based on when the server was last booted). This version allows two types of entries: timers based on an interval, e.g. every 10 seconds (which would start on the minute, so occur at 00:00s, 00:10s, 00:20s etc) and fixed timers occuring once per parent-interval. e.g. a fixed 10s timer would occur at 00:10s, 01:10s etc. (ie its a one minute interval happening 10seconds after every minute). --- mod_cron/src/mod_cron.erl | 107 +++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 14 deletions(-) diff --git a/mod_cron/src/mod_cron.erl b/mod_cron/src/mod_cron.erl index 76a6087..af473a9 100644 --- a/mod_cron/src/mod_cron.erl +++ b/mod_cron/src/mod_cron.erl @@ -16,6 +16,8 @@ run_task/3, web_menu_host/3, web_page_host/3, start/2, + apply_interval/3, + apply_interval1/3, stop/1]). -include("ejabberd_commands.hrl"). @@ -27,7 +29,6 @@ -record(task, {taskid, timerref, host, task}). - %% --------------------- %% gen_mod %% --------------------- @@ -52,25 +53,103 @@ stop(Host) -> %% Task management %% --------------------- +time_to_ms(IntervalUnit, IntervalNum) -> + case IntervalUnit of + seconds -> timer:seconds(IntervalNum); + minutes -> timer:minutes(IntervalNum); + hours -> timer:hours(IntervalNum); + days -> timer:hours(IntervalNum)*24 + end. + +time_until_event(IntervalMS) -> + {MegaSecs, Secs, MicroSecs} = erlang:now(), + NowMS = (MegaSecs*1000000 + Secs)*1000 + round(MicroSecs/1000), + MSSinceLastEvent = (NowMS rem IntervalMS), + (IntervalMS - MSSinceLastEvent). + +begin_interval_timer(TaskId, TimeUnit, TimeNum, StartParams) -> + IntervalMS = time_to_ms(TimeUnit, TimeNum), + MSToGo = time_until_event(IntervalMS), + {ok, TimerRef} = timer:apply_after(MSToGo, ?MODULE, apply_interval, + [TaskId, IntervalMS, StartParams]), + TimerRef. + +begin_fixed_timer(TaskId, TimeUnit, TimeNum, StartParams) -> + %% A fixed second timer happens minutely, minute timer happens hourly, a fixed hour timer happens daily. + IntervalMS = case TimeUnit of + seconds -> timer:minutes(1); + minutes -> timer:hours(1); + hours -> timer:hours(1) * 24; + _ -> undefined + end, + + FixedTimeMS = time_to_ms(TimeUnit, TimeNum), + + %% Calculate time until the next IntervalUnit, then add FixedTimeMS + %% e.g. now is 00:00:32 wait until the next minute (28s), then keep waiting + %% 5 more seconds to get 00:01:05 (= wait 33s). + %% We then fire the event at 00:01:05 and wait a minute to fire again + %% at 00:02:05 etc. + MSToGo1 = time_until_event(IntervalMS) + FixedTimeMS, + + %% If we were, for example, at 1:03PM and the event is hourly at + %% 1:05PM then we dont want to wait 57+5 minutes, we want to wait + %% 2 minutes. + MSToGo2 = if MSToGo1 > IntervalMS -> + MSToGo1 - IntervalMS; + true -> + MSToGo1 + end, + + ?DEBUG("MS To Go Fixed: ~p ~p", [MSToGo1, MSToGo2]), + {ok, TimerRef} = timer:apply_after(MSToGo2, ?MODULE, apply_interval, [TaskId, IntervalMS, StartParams]), + TimerRef. + +apply_interval(TaskId, IntervalMS, StartParams) -> + %% apply_after doesnt belong to a pid (which is needed for apply_after to stay alive), so make one + spawn(?MODULE, apply_interval1, [TaskId, IntervalMS, StartParams]). + +apply_interval1(TaskId, IntervalMS, [M, F, A]=StartParams) -> + % we've already waited for the interval to expire once to get here, + % and apply_interval doesn't apply first, so run the task once then start the timer + run_task(M, F, A), + {ok, TimerRef} = timer:apply_interval(IntervalMS, ?MODULE, run_task, StartParams), + update_timer_ref(TaskId, TimerRef), + + %% Wait forever so the timer process stays alive + receive + _ -> + ok + end. + +update_timer_ref(TaskId, NewTimerRef) -> + [Task] = ets:lookup(cron_tasks, TaskId), + NewTask = Task#task{timerref=NewTimerRef}, + ets:insert(cron_tasks, NewTask). + %% Method to add new task add_task(Host, Task) -> - [Time_num, Time_unit, Mod, Fun, Args] = - [proplists:get_value(Key, Task) || Key <- [time, units, module, function, arguments]], - - %% Convert to miliseconds - Time = case Time_unit of - seconds -> timer:seconds(Time_num); - minutes -> timer:minutes(Time_num); - hours -> timer:hours(Time_num); - days -> timer:hours(Time_num)*24 - end, - - %% Start timer - {ok, TimerRef} = timer:apply_interval(Time, ?MODULE, run_task, [Mod, Fun, Args]), + [TimeNum, TimeUnit, Mod, Fun, Args, InTimerType] = + [proplists:get_value(Key, Task) || Key <- [time, units, module, function, arguments, timer_type]], + TimerType = case InTimerType of + <<"fixed">> -> + fixed; + fixed -> + fixed; + _ -> + interval + end, %% Get new task identifier TaskId = get_new_taskid(), + TimerRef = case TimerType of + interval -> + begin_interval_timer(TaskId, TimeUnit, TimeNum, [Mod, Fun, Args]); + fixed -> + begin_fixed_timer(TaskId, TimeUnit, TimeNum, [Mod, Fun, Args]) + end, + %% Store TRef Taskr = #task{ taskid = TaskId, From 566b78849ee5e95aac9c2b375d233b0c8d9872aa Mon Sep 17 00:00:00 2001 From: Zach Goldberg Date: Wed, 11 Feb 2015 14:26:35 -0800 Subject: [PATCH 2/2] Fix README documentation for arguments --- mod_cron/README.txt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/mod_cron/README.txt b/mod_cron/README.txt index a7d6ffa..483b332 100644 --- a/mod_cron/README.txt +++ b/mod_cron/README.txt @@ -31,8 +31,16 @@ Each task is described with five elements: * Time is an integer. * Units indicates the time unit you use. It can be: seconds, minutes, hours, days. * Module and * Function are the exact call you want to schedule. -* Arguments is an erlang list of arguments inside the characters "> ." +* Arguments is an array. Strings will be converted to binaries. +* timer_type is one of 'fixed' or 'interval'. Fixed timers occur at a fixed time + after the [minute|hour|day] e.g. every hour on the 5th minute (1:05PM, 2:05PM etc) + interval timers occur every interval (starting on an even unit) e.g. every 10 minutes + starting at 1PM, 1:10PM, 1:20PM etc. + Fixed timers are the equivalent of unix cron's comma syntax e.g. "2 * * *" and interval + timers are the / syntax e.g. "*/5 * * *". + + Default timer_type is interval. EXAMPLE TASKS ============= @@ -45,12 +53,17 @@ modules: units: hours module: mnesia function: info - arguments: "> []." + arguments: {} + timer_type: fixed - time: 10 units: seconds module: ejabberd_auth function: try_register - arguments: "> [\"user1\", \"localhost\", \"somepass\"]." + arguments: + - "user1" + - "localhost" + - "somepass" + timer_type: interval EJABBERD COMMANDS