New timer app

Ulf Wiger <>
Thu Apr 13 15:33:43 CEST 2000


On Mon, 10 Apr 2000, Sean Hinde wrote:

Sean>All,
Sean>
Sean>I recently posted a little application derived from the
Sean>standard timer module in Kernel which uses an ordered_set
Sean>mnesia table with a couple of indeces to give something
Sean>which is more scalable.
Sean>
Sean>I have two versions of this though, one based on an mnesia
Sean>table, and one using three ets tables to do the same.
Sean>
Sean>My question is really which one are people going to find
Sean>more useful (if at all ;-))? Pretty much everything I write
Sean>has a big dollop of mnesia at the core but I guess that is
Sean>not the general case?
Sean>
Sean>In fact I think I might have persuaded myself the ets one is
Sean>better! Views?

I must have dozed off for a while -- I didn't realize what you'd 
done until now.

At AXD 301, we have forbidden use of the timer module, and recommend
using our own module, sysTimer. The reason is that timer.erl is
very inefficiently implemented. We discovered that it was much
(rougly 10x) cheaper to use the classic schoolbook example:

send_after(Time, Msg) ->
  spawn_link(?MODULE, do_send_after, [self(), Time, Msg]).

do_send_after(Pid, Time, Msg) ->
   receive
   after Time ->
      Pid ! {timeout, self(), Msg}
   end.

Since then, OTP has implemented support for timers as BIFs, and
our sysTimer module now uses the BIFs instead.

I've attached our sysTimer module for your perusal.
Note that it contains some other functions, like
send_after_specified(DateTime). It is not API-compatible with
timer.erl, and uses a special mnesia table for "logical timers".
If you want to use it, you can rewrite parts to make it run outside of
AXD 301.

/Uffe
-- 
Ulf Wiger, Chief Designer AXD 301         <>
Ericsson Telecom AB                          tfn: +46  8 719 81 95
Varuvägen 9, Älvsjö                          mob: +46 70 519 81 95
S-126 25 Stockholm, Sweden                   fax: +46  8 719 43 44
-------------- next part --------------
%%% #--------------------------------------------------------------------------
%%% #0.    BASIC INFORMATION
%%% #--------------------------------------------------------------------------
%%% %CCaseFile:	sysTimer.erl %
%%% Description: Handles system timers. See MID sysTimer_MID.fm
%%%              for information.
%%%              However, note that all functions in sysTimer uses
%%%              utc (universal time). This means that all specified
%%%              times must be specified in utc when calling sysTimer.
%%%              Currently, there is no support for converting
%%%              local time to universal time (day-live saving ..)
%%%              in sysTimer or in OTP.
%%%
%%% Modules used:  mnesia, calendar, erlang, sysTimezone
%%% ----------------------------------------------------------

-module(sysTimer).
%-behaviour(timer).
-id('19/190 55-CNA 121 70').
-vsn('/main/Inc3/Inc4/Inc5/R2A/R3B/R4A/R6A/3').
-date('99-10-05').
-author('etxtopi').


%%% ----------------------------------------------------------
%%% Template Id: ETX/B 00201 - 19055/1 Rev C
%%%
%%% #Copyright (C) 1996
%%% by ERICSSON TELECOM AB
%%% S - 125 26  STOCKHOLM
%%% SWEDEN, tel int + 46 8 719 0000
%%% 
%%% The program may be used and/or copied only with the written permission from
%%% ERICSSON TELECOM AB, or in accordance with the terms and conditions
%%% stipulated in the agreement/contract under which the program has been
%%% supplied.
%%%   
%%% All rights reserved.
%%% 
%%%
%%% #--------------------------------------------------------------------------
%%% #1.    REVISION LOG
%%% #--------------------------------------------------------------------------
%%% Rev      Date       Name        What
%%% -----    -------    --------    --------------------------
%%% PA1      960814     epkolso     Created.
%%% PA2      960815     etxberb     Correction.
%%% PA3      960816     etxberb     System messages added.
%%% PA4      960819     etxberb     An mnesia-read operation added to function
%%%                                 start_server/1.
%%% PA5      960822     etxberb     The return value from function start_server
%%%                                 changed to {ok,Pid}.
%%% PA6      960829     etxberb     SYS_DEBUG added.
%%% PA7      960916     epkolso     Changed module name to sysTimer.
%%%                                 Added start_interval.
%%%                                 Took out all ugly mnesia reads and
%%%                                 system dependent stuff to make it more
%%%                                 general, useful, and beautiful.
%%% PA8      961016     epkulwi     Added sleep, apply_after, etc.
%%%                                 Changed names and certain behavior.
%%% PA9      970304     epkulwi     Changed behaviour of xxx_interval,
%%%                                 Added sysTimeout().
%%% Inc4/1   970617     etxuwig     Changed apply_interval to be exact.
%%% Inc4/4   970901     etxtryd     Added apply_timer_specified
%%% Inc4/6   970910     etxtryd     Test before spwn in apply_timer_specified
%%% Inc4/7   970911     etxtryd     export wait_a_day
%%% Inc4/8   970914     etxtryd     Updates comments by etxuwig
%%% Inc4/9   971111     etxtryd     Added send_after_specified
%%% Inc4/10  971203     etxtryd     Uses utc instead of local time.
%%% Inc5/1   980130     etxtopi     Added convert_2utc
%%% Inc5/2   980505     etxtopi     convert_2utc calls erlang
%%% R2A/1    980507     etxuwig     changed dirty_read() to ets:lookup()
%%% R3B/2    981007     etxtopi     convert_2utc has to entries now.
%%% R3B/4    981103     etxuwig     added put() statements for debugging
%%% R3B/5    981106     etxuwig     timer BIFs, code_change in interval timers
%%% R6A/1    990614     etxtryd     changed to dirty_read() from ets:lookup() 
%%% R6A/2    990705     etxtryd     ets:lookup() back again
%%% R6A/3    991004     etxtopi     New convert_2utc
%%% #--------------------------------------------------------------------------
%%% #2.    EXPORT LISTS
%%% #--------------------------------------------------------------------------
%%% #2.1   EXPORTED INTERFACE FUNCTIONS
%%% #--------------------------------------------------------------------------

-export([send_after/2,
	 send_after_specified/2,
         send_interval/2,
         apply_after/4,
         apply_after_specified/4,
         apply_interval/4,
	 convert_2utc/2,
	 convert_2utc/1,
	 cancel/1,
         sleep/1,
         set_value/2,    
         get_value/1,
         system_time/0]).

-export([sysTimeout/0]).

%%% #--------------------------------------------------------------------------
%%% #2.2   EXPORTED INTERNAL FUNCTIONS
%%% #--------------------------------------------------------------------------
-export([do_send_after/3,
	 do_send_interval/3,
         do_apply_after/5,
	 do_apply_after_specified/5,
         do_apply_interval/6,
	 wait_a_day/3,
	 wait_a_day/5]).

%%% #--------------------------------------------------------------------------
%%% #2.3   DEFINITION OF CONSTANTS
%%% #--------------------------------------------------------------------------

-include("sysDefines.hrl").


-record(sysTimer, {key, value}).

%%% #--------------------------------------------------------------------------
%%% #2.4   DEFINITION OF CONSTANTS AS MACROS
%%% #--------------------------------------------------------------------------

%%% #--------------------------------------------------------------------------
%%% #3.    CODE
%%% #--------------------------------------------------------------------------
%%% #3.1   CODE FOR EXPORTED INTERFACE FUNCTIONS
%%% #--------------------------------------------------------------------------

%%%----------------------------------------------------------------------
%%% -type sysTimeout()->
%%%     integer().
%%% Input: -
%%% Output: Value
%%% Exceptions: 
%%% Description: Reads the value stored as sysTimeout in Mnesia,
%%%              or returns a hardcoded default.
%%%----------------------------------------------------------------------
sysTimeout() ->
    case catch ets:lookup(sysTimer, sysTimeout) of
	[#sysTimer{value = Time}] ->
		Time;
	 _ ->
		10000
	end.

%%%----------------------------------------------------------------------
%%% -type set_value(Name : atom(), Time : integer())->
%%%     term().
%%% Input: Name, Time
%%% Output: ok
%%% Exceptions: Exits if Mnesia isn't running.
%%% Description: Sets named timer Name to timeout value Time.
%%%              Specifically, it stores the relation in the Mnesia
%%%              table sysTimer (which it creates if it isn't there.)
%%%----------------------------------------------------------------------
set_value(Name, Time) when atom(Name), integer(Time), Time >= 0 ->
    ok = verify_table(),
    mnesia:dirty_write(#sysTimer{key = Name, value = Time}).


get_value(Name) ->
    case ets:lookup(sysTimer, Name) of
	[#sysTimer{value = Time}] ->
	    Time;
	_ ->
	    exit(undef)
    end.

%%%----------------------------------------------------------------------
%%% -type send_after(TimerSpec : term(), Msg : term())->
%%%     term().
%%% Input: TimerSpec, Msg
%%% Output: Key
%%% Exceptions: Exit if named timer hasn't been defined.
%%% Description: TimerSpec specifies the timeout value, either
%%%              indirectly by name or directly in milliseconds.
%%%              Send a message after timeout given by TimerSpec.
%%%              The format of the message is {timeout, Key, Msg}.
%%%----------------------------------------------------------------------
%send_after(Time, Msg) when integer(Time) ->
%    spawn_link(sysTimer,do_send_after,[Time, self(), Msg]);
%send_after(Name, Msg) ->
%    spawn_link(sysTimer, do_send_after, [get_value(Name), self(), Msg]).

send_after(Time, Msg) when integer(Time) ->
    erlang:start_timer(Time, self(), Msg);
send_after(Name, Msg) ->
    erlang:start_timer(get_value(Name), self(), Msg).

%%%----------------------------------------------------------------------
%%% -type send_after_specified(TimerSpec : {{year, month, day},
%%%                            {hour, minute, second}}, Msg : term()) ->
%%%     term().
%%% Input: TimerSpec (all values are integers), Msg
%%% Output: Key | invalid_timer_specified
%%% Exceptions: -
%%% Description: Send a message after timeout given by TimerSpec.
%%%              The format of the message is {timeout, Key, Msg}.
%%%              TimerSpec is specified in calender:universal_time()
%%%              format.
%%%----------------------------------------------------------------------
send_after_specified({{SYear,SMonth,SDay}, {SHour, SMin, SSec}}, Msg) ->
    case calendar:time_difference(calendar:universal_time(),
				  {{SYear,SMonth,SDay},
				   {SHour, SMin, SSec}}) of
	{DDate, {DHour, DMin, DSec}} ->
	    if
		DDate < 0 -> % Faulty timer specified
		    invalid_timer_specified;
		DDate == 0 -> % Same day
		    Time = ((DHour*3600) + (DMin*60) + DSec) * 1000,
		    send_after(Time, Msg);
%		    spawn_link(sysTimer,
%			       do_send_after, [Time, self(), Msg]);
		DDate > 0 -> % Start by waiting a day
		    spawn_link(sysTimer, wait_a_day,
			       [{{SYear,SMonth,SDay}, {SHour, SMin, SSec}},
				Msg, self()])
	    end
    end;
send_after_specified(FaultyTimeSpecified, Msg) ->
    invalid_timer_specified.


%%%----------------------------------------------------------------------
%%% -type send_interval(TimerSpec : term(), Msg : term())->
%%%     term().
%%% Input: TimerSpec, Msg
%%% Output: Key
%%% Exceptions: Exit if named timer hasn't been started.
%%% Description: TimerSpec specifies the timeout value, either
%%%              indirectly by name or directly in milliseconds.
%%%              Send a message at intervals given by TimerSpec.
%%%              The message sent is {timeout, Key, Msg}.
%%%              If Time is a logical Timer name, the timer thread
%%%              will re-read the actual timer value at each interval.
%%%              Thus, a call to sysTimer:set_value(Name, Value) will
%%%              be reflected in each running interval thread which
%%%              uses that logical timer.
%%%----------------------------------------------------------------------
%send_interval(Time, Msg) when integer(Time) ->
%    spawn_link(sysTimer,do_send_interval,[Time, self(), Msg]);
send_interval(NameOrTime, Msg) ->
    spawn_link(sysTimer, do_send_interval, [NameOrTime, self(), Msg]).

%%%----------------------------------------------------------------------
%%% -type cancel(Key : term())->
%%%     term().
%%% Input: Key
%%% Output: ok
%%% Exceptions: 
%%% Description: Cancels timer request identified by Key.
%%%              If the timeout message has already been sent, it is
%%%              removed from the message queue.
%%%----------------------------------------------------------------------
cancel(Pid) when pid(Pid) ->
    unlink(Pid),
    exit(Pid, kill),
    receive
	{timeout, Pid, _} ->
	    ok
    after 0 ->
	    ok
    end;
cancel(TimerRef) ->
    case erlang:cancel_timer(TimerRef) of
	false ->
	    receive
		{timeout, TimerRef, _} ->
		    ok
	    after 0 ->
		    ok
	    end;
	_ ->
	    ok
    end.

%%%----------------------------------------------------------------------
%%% -type apply_after(TimerSpec : term(), 
%%%                   M : atom(), F : atom(), A : list())->
%%%     term().
%%% Input: TimerSpec, Module, Function, Args
%%% Output: Key
%%% Exceptions: 
%%% Description: Apply function {Module, Function, Args} after Time.
%%%              This call is made from within a separate process.
%%%----------------------------------------------------------------------
apply_after(Time, M, F, A) when integer(Time) ->
    spawn_link(sysTimer, do_apply_after, [Time, M, F, A, self()]);
apply_after(Name, M, F, A) ->
    spawn_link(sysTimer, do_apply_after, [get_value(Name), M, F, A, self()]).


%%%----------------------------------------------------------------------
%%% -type apply_after_specified(TimerSpec : {{year, month, day},
%%%                            {hour, minute, second}}, M : atom(),
%%%                            F : atom(), A : list())->
%%%     term().
%%% Input: TimerSpec (all values are integers), Module, Function, Args
%%% Output: Key | invalid_timer_specified | already_expired
%%% Exceptions: 
%%% Description: Apply function {Module, Function, Args} when the 
%%%              current time (calendar:universal_time() is equal to the
%%%              specified TimerSpec.
%%%              This call is made from within a separate process.
%%%----------------------------------------------------------------------
apply_after_specified({{SYear,SMonth,SDay}, {SHour, SMin, SSec}}, M, F, A) ->
    case calendar:time_difference(calendar:universal_time(),
				  {{SYear,SMonth,SDay}, 
				   {SHour, SMin, SSec}}) of
	{DDate, {DHour, DMin, DSec}} ->
	    if
		DDate < 0 -> % Faulty timer specified
		    already_expired;
		DDate == 0 -> % Same day
		    Time = ((DHour*3600) + (DMin*60) + DSec) * 1000,
%		    spawn_link(sysTimer, do_apply_after, [Time, M, F, A,
%							  self()]);
		    apply_after(Time, M, F, A);
		DDate > 0 -> % Start by waiting a day
		    spawn_link(sysTimer, wait_a_day,
			       [{{SYear,SMonth,SDay}, {SHour, SMin, SSec}},
				M, F, A, self()])
	    end
    end;
apply_after_specified(FaultyTimeSpecified, M, F, A) ->
    invalid_timer_specified.

%%%----------------------------------------------------------------------
%%% -type apply_interval(Time : integer(), 
%%%                      M : atom(), F : atom(), A : list())->
%%%     term().
%%% Input: Time, Module, Function, Args
%%% Output: Key
%%% Exceptions: 
%%% Description: Apply function {Module, Function, Args} repeatedly
%%%              after Time.
%%%              This call is made from within a separate process.
%%%              The timer makes sure that the interval stays exact
%%%              relative to the start time. If the applied function 
%%%              doesn't return until an interval has been passed, the 
%%%              timer will wait until the next interval.
%%%              If Time is a logical Timer name, the timer thread
%%%              will re-read the actual timer value at each interval.
%%%              Thus, a call to sysTimer:set_value(Name, Value) will
%%%              be reflected in each running interval thread which
%%%              uses that logical timer.
%%%----------------------------------------------------------------------
%apply_interval(Time, M, F, A) when integer(Time) ->
%    Pid = spawn_link(sysTimer, do_apply_interval, 
%		[Time, M, F, A]);
apply_interval(NameOrTime, M, F, A) ->
    Now = erlang:now(),
    Pid = spawn_link(sysTimer, do_apply_interval, 
		[NameOrTime, Now, Now, M, F, A]).


sleep(Time) ->
    receive
    after Time ->
	    ok
    end.

%%%----------------------------------------------------------------------
%%% -type convert_2utc(Date : term(), Time : term())->
%%%     term().
%%% Input: {YYYY,MM,DD},{HH,MM,SS}
%%% Output: {{NewDate}, {NewTime}}
%%% Exceptions: -
%%% Description: Converts the localtime to the corresponding UTC-time
%%% (universal_time). This method does not use local_time_to_universal_time
%%% which gives you faulty results when using non-existing times (eg.
%%% a time in the "DST/STD gap").
%%%
%%%----------------------------------------------------------------------
convert_2utc(Date, Time) ->
     convert_2utc({Date, Time}).

convert_2utc({Date, Time}) ->
    %% stolen from lole
    D1 = calendar:datetime_to_gregorian_seconds(calendar:local_time()),
    D2 = calendar:datetime_to_gregorian_seconds({Date, Time}),
    Diff = D2-D1,
    D3 = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
    D4 = D3+Diff,
    calendar:gregorian_seconds_to_datetime(D4).

%%%
%%% return the system time in
%%% milli-seconds.
%%%
system_time() ->
    {M,S,U} = erlang:now(), 
    1000000000 * M + 1000 * S + (U div 1000).

%%
%% Calculate difference between two timestamps.
%% It's important to avoid using bignums (>16#FFFFFF).
%%
diff({M,S,U}, {M,S1,U1}) ->
    ((S-S1) * 1000) + ((U-U1) div 1000);
diff({M,S,U}, {M1,S1,U1}) ->
    ((M-M1)*1000000+(S-S1))*1000 + ((U-U1) div 1000).

%%
%% add(Time, Timestamp) -> Timestamp1
%%   This function adds Time milliseconds to a 
%%   timestamp and returns a new timestamp.
%%   This is used by apply_interval to maintain
%%   constant intervals. Timestamp1 should always
%%   be of type (StartTime + N*Time).
%%
add(Time, {M,S,U}) ->
    U1 = (U + Time * 1000),
    S1 = (S + (U1 div 1000000)),
    {M + S1 div 1000000, 
     S1 rem 1000000, 
     U1 rem 1000000}.

%%
%% sleep_time(Time, Acc) -> {TimeToSleep, Acc1}
%%   Time ::= timeout in milliseconds
%%   Acc  ::= accumulated timestamp
%%   This function adds Time to Acc and calculates
%%   how many milliseconds remain to Acc1. 
%%   Assuming that Time is constant Acc1 will always
%%   be the next (StartTime + N*Time) in the future,
%%   that is, an interval may be skipped for whatever
%%   reason, and sleep_time/2 will keep up.
%%
sleep_time(Time, Acc) ->
    Acc1 = add(Time, Acc),
    case diff(Acc1,erlang:now()) of
	N when N > 0 ->
	    {normal, N, Acc1};
	Less ->
	    Time1 = Time * ((-Less div Time)+1),
	    {skip, Time1 + Less, add(Time1+Time, Acc)}
    end.

%%% #--------------------------------------------------------------------------
%%% #3.2   CODE FOR EXPORTED INTERNAL FUNCTIONS
%%% #--------------------------------------------------------------------------

do_send_after(Time, Pid, Msg) ->
    put(arguments, [Time, Pid, Msg]),
    receive
    after Time ->
	    Pid ! {timeout, self(), Msg},
	    unlink(Pid)
    end.

do_send_interval(Time, Pid, Msg) when integer(Time) ->
    put(arguments, [Time, Pid, Msg]),
    receive
    after Time ->
	    Pid ! {timeout, self(), Msg},
	    ?MODULE:do_send_interval(Time, Pid, Msg)
    end;
do_send_interval(Name, Pid, Msg) ->
    put(arguments, [Name, Pid, Msg]),
    Time = get_value(Name),
    receive
    after Time ->
	    Pid ! {timeout, self(), Msg},
	    ?MODULE:do_send_interval(Name, Pid, Msg)
    end.

do_apply_after(Time, M, F, A, LinkedPid) ->
    put(arguments, [Time, M, F, A, LinkedPid]),
    receive
    after Time ->
            apply(M, F, A),
	    unlink(LinkedPid)
    end.


wait_a_day(SpecifiedTime, M, F, A, LinkedPid) ->
    put(arguments, [SpecifiedTime, M, F, A, LinkedPid]),
    receive
    after (24*3600*1000) ->  % One day in milliseconds
	    do_apply_after_specified(SpecifiedTime, M, F, A, LinkedPid)
    end.

wait_a_day(SpecifiedTime, Msg, UserPid) ->
    put(arguments, [SpecifiedTime, Msg, UserPid]),
    receive
    after (24*3600*1000) ->  % One day in milliseconds
	    do_send_after_specified(SpecifiedTime, Msg, UserPid)
    end.

do_apply_after_specified(SpecifiedTime, M, F, A, LinkedPid) ->
    put(arguments, [SpecifiedTime, M, F, A, LinkedPid]),
    case calendar:time_difference(calendar:universal_time(), SpecifiedTime) of
	{Date, {Hour, Min, Sec}} ->
	    if
		Date == 0 -> % Same day
		    Time = ((Hour*3600) + (Min*60) + Sec) * 1000,
		    do_apply_after(Time, M, F, A, LinkedPid);
		Date > 0 -> % Day in the future
		    wait_a_day(SpecifiedTime, M, F, A, LinkedPid);
		Date < 0 ->
		    % This clause can occur if the SpecifiedTime is
		    % day(s) + 1 (or some) second and the load is
		    % very high.
		    do_apply_after(1, M, F, A, LinkedPid)
	    end
    end.

do_send_after_specified(SpecifiedTime, Msg, UserPid) ->
    put(arguments, [SpecifiedTime, Msg, UserPid]),
    case calendar:time_difference(calendar:universal_time(), SpecifiedTime) of
	{Date, {Hour, Min, Sec}} ->
	    if
		Date == 0 -> % Same day
		    Time = ((Hour*3600) + (Min*60) + Sec) * 1000,
		    do_send_after(Time, UserPid, Msg);
		Date > 0 -> % Day in the future
		    wait_a_day(SpecifiedTime, Msg, UserPid);
		Date < 0 ->
		    % This clause can occur if the SpecifiedTime is 
		    % day(s) + 1 (or some) second and the load is
		    % very high.
		    do_send_after(1, Msg, UserPid)
	    end
    end.


do_apply_interval(Time, Start, Acc, M, F, A) when integer(Time) ->
    put(arguments, [Time, Start, Acc, M, F, A]),
    {Mode, Sleep, Acc1} = sleep_time(Time, Acc),
    receive
    after Sleep ->
            apply(M, F, A),
	    ?MODULE:do_apply_interval(Time, Start, Acc1, M, F, A)
    end;
do_apply_interval(Name, Start, Acc, M, F, A) ->
    put(arguments, [Name, Start, Acc, M, F, A]),
    Time = get_value(Name),
    {Mode, Sleep, Acc1} = sleep_time(Time, Acc),
    receive
    after Sleep ->
            apply(M, F, A),
            ?MODULE:do_apply_interval(Name, Start, Acc1, M, F, A)
    end.

%%% #--------------------------------------------------------------------------
%%% #3.3   CODE FOR INTERNAL FUNCTIONS
%%% #--------------------------------------------------------------------------
verify_table() ->
    case catch mnesia:table_info(sysTimer, name) of
	{'EXIT', _} ->
	    case catch mnesia:system_info(db_nodes) of
		{'EXIT', _} ->
		    exit({error, mnesia_not_running});
		Nodes ->
		    {atomic, ok} = 
			mnesia:create_table([{name, sysTimer},
					     {disc_copies, Nodes},
					     {attributes, [key, value]}]),
		    ok
	    end;
	_ ->
	    ok
    end.



%%% #--------------------------------------------------------------------------
%%% #4     CODE FOR TEMPORARY CORRECTIONS
%%% #--------------------------------------------------------------------------


%%% ----------------------------------------
%%% end of sysTimer.erl


More information about the erlang-questions mailing list