Periodic sending a message based on clock wall time

Hello,

Suppose that a system (let’s say OTP 24) configured with multi-time warp mode (Erlang -- Time and Time Correction in Erlang).

I need to start a specific task at clock wall UTC 12:00:00 and continuing executing that task periodically each 15 minutes. (12:00:00 12:15:00 12:30:00 12:45:00 … and so on).

How could I make it possible ?

One approach would be: when starting my gen_server or gen_statem, I calculate the delta time from the erlang:system_time/1 converted to calendar calendar:system_time_to_universal_time/1 like this:

Ts = erlang:system_time(millisecond),
Time = calendar:system_time_to_universal_time(Ts, millisecond)}.

Then, the next step is to find how many seconds is necessary to the next quarter hour (15min),

{_Hour, Minute, Second} = Time,
MSeconds = Minute * 60,
TSeconds = MSeconds + Second,
CurrentQuarterSeconds = erlang:round(math:ceil(TSeconds / Period) * Period),
NextQuarter = CurrentQuarterSeconds - TSeconds.

Now, I know a specific point in time to start. Getting the current time and adding it to the NextQuarter should work as expected:

Now = erlang:monotonic_time(millisecond),
T = Now + NextQuarter.

erlang:start_timer(T, self(), quarter_timeout, [{abs, true}]).

When I receive the first quarter_timeout message, then I just need to schedule another start_timer:

Now = erlang:monotonic_time(millisecond),
T = Now + timer:seconds(900),
erlang:start_timer(T, self(), quarter_timeout, [{abs, true}]).

Done, from now and beyond my process will receive a ‘quarter_timeout’ message each 15 minutes.

But, I guess it will not work like that. For sure I’m missing a couple of things here. Like milliseconds when I calculate the next periods.

Any thoughts ?

Why you can’t use timer:send_interval/3 (or timer:apply_interval/4)?

I guess that having timeouts at even quarters of an UTC hour are important and not just having timouts every 15 minutes. If every 15 minutes are good enough, timer:send_interval/3 as suggested above will do the trick.

There is no need to convert via UTC. Erlang system time is Erlang’s view of POSIX time. Since POSIX time (Unix time) at zero was at midnight and every day is exactly 86400 seconds in POSIX time you can easily calculate next even quarter like this:

next_timeout(SysTime, %% Erlang system time in millisecond time unit
             TimeOffs %% Erlang time offset in millisecond time unit
            ) ->
    Q = 15*60*1000, %% Number of milliseconds during 15 minutes
    Timeout = (SysTime div Q + 1) * Q - TimeOffs,
    Timeout. %% An absolute monotonic time in millisecond time unit

You’ll get the timeout as an absolute monotonic time in millisecond time unit which can be used when setting the timeout using erlang:send_after/4 or erlang:start_timer/4 using the {abs, true} option.

When a timeout has occurred you don’t want to look up current time and add 15 minutes to it in order to set the next timeout. In this case timeouts will be delayed more and more due to the time it takes for the process being scheduled and execute up to the point where it looks up current time. Instead you want to either call next_timeout() again or simply add Q to the previous timeout time. This is the reason why the {abs, true} option was introduced. Previously you could only set timers relative to current time.

Without adjusting for time warps your timeouts will eventually become off though. Even if your system have the system clock perfectly set when you start and it has an exact frequency all the time, leap seconds will eventually mess up your timeouts.

Since POSIX time and UTC are to align and UTC may have 86399-86401 seconds a day, due to leap seconds being added or removed, a POSIX time second is not always an SI second long. When a leap second is added, (a) certain POSIX time second(s) will be longer than an SI second and when a leap second is removed (has never happened as of now) (a) certain POSIX second(s) will be shorter than an SI second. Introduction of a leap second will cause a time warp and will cause your timeouts to become off by a second unless you adjust for it.

By monitoring the time offset, using erlang:monitor(time_offset, clock_service), you can adjust the timeout when a time warp occurs. You’ll receive a {'CHANGE', MonitorRef, time_offset, clock_service, NewTimeOffset} message when time offset has changed. Note that NewTimeOffset is in native time unit, so you want to convert it to millisecond time unit, using erlang:convert_time(NewTimeOffset, native, millisecond), before calculating a new timeout using the next_timeout/2 function above. Cancel the old timeout and set the newly calculated timeout.

Note that timeouts may get a bit off when the time warp occurs close to when a timeout should occur, but your timeout won’t slowly get more and more off.

4 Likes

Forgot the initial timeout:

first_timeout(HourNr) ->
    SysTime = erlang:system_time(millisecond),
    TimeOffs = erlang:time_offset(millisecond),
    DayMs = 24*60*60*1000,
    HourNrMs = HourNr*60*60*1000,
    Timeout = ((SysTime + DayMs - HourNrMs) div DayMs) * DayMs + HourNrMs - TimeOffs,
    Timeout. %% An absolute monotonic time in millisecond time unit

first_timeout(12) will give you the absolute monotonic time for the first timeout (when the clock gets to 12:00 UTC the next time).

1 Like

I got the explanation, crystal clear. Thanks.

I’ll isolate the code that I’m working on and post it here for historic purposes.

So, just to confirm a new call to next_timeout/2 would be like:

next_timeout(erlang:system_time(millisecond), NewTimeOffset)

Thanks.

When you are delivered the timeout message of the previous timeout, it is cheaper to just add 15 minutes as milliseconds (value of Q in next_timeout()) to the old timeout value. If you want to call next_timeout(), it should be called with the old time offset alternatively:

next_timeout(erlang:system_time(millisecond), erlang:time_offset(millisecond))

After receiving a 'CHANGE' message (due to monitoring of time offsett), the call calculating the new timeout should look like this:

next_timeout(erlang:system_time(millisecond),
             erlang:convert_time_unit(NewTimeOffset, native, millisecond))

(The call to erlang:system_time(millisecond) could have been made inside of next_timeout() instead of requiring this to be passed as argument. I don’t know why I made it take system time as an argument…)

Not sure what your need is driven by, but there’s an erlcron application, which is designed for similar needs of calling functions on periodic scheduling.

1 Like

Looks interesting. If my requirements had more timers I would definitively use it.

Hi,

Here is what I’m trying to implement. I’ve extracted the implementation and created a working (as well if all of my requirements) example: GitHub - joaohf/pmclock: Performance Monitoring Clock, G.7710

The pmclock_statem is very simple and uses features from gen_statem to run generic timers for each period (15-min and 24-hour).

The code works. I’m still testing it. However, when I run the code in the real target hw, I’m having too much messages from clock_service. I don’t know why yet. If I run the code in Windows WSL and/or native linux I seldom get clock_service messages. Looks like the real target has something weird.