gleam-lang / suggestions

📙 A place for ideas and feedback
26 stars 2 forks source link

OTP timer module wrapper #94

Open edkelly303 opened 3 years ago

edkelly303 commented 3 years ago

As discussed on Discord with @lpil :

We could also do with something like https://erlang.org/doc/man/timer.html#send_interval-2

I'm guessing an implementation could look something like this:

external fn erlang_send_interval(Int, Pid, msg) -> Timer =
  "erlang" "send_interval"

/// Send a message over a channel repeatedly, with a specified time interval between each send.
///
pub fn send_interval(sender: Sender(msg), interval: Int, message: msg) -> Timer {
  case sender.prepare {
    Some(prepare) -> erlang_send_interval(interval, sender.pid, prepare(message))
    None -> fake_timer()
  }
}
lpil commented 3 years ago

It looks like the Erlang function can return an error, but it doesn't document why this might happen. I would be interested in knowing more about that

edkelly303 commented 3 years ago

Oh, sorry, I missed that! (I am probably out of my depth here.)

Looking at the Erlang source, it seems that send_interval calls apply_interval, which returns an {error, badarg} if its To argument isn't really a Pid or a registered name.

https://github.com/erlang/otp/blob/fed83073632b65620dfed61ede6648062c870d4c/lib/stdlib/src/timer.erl#L294

I guess this isn't a concern for us because we know our Sender argument must contain a valid Pid?

lpil commented 3 years ago

Well spotted! Yes I think we can safely ignore that then.

edkelly303 commented 3 years ago

I did some more research on this, and the plot thickens!

There are (at least) two different ways of doing delayed sends in Erlang using functions from the standard library, and they are built on two completely different implementations.

The Gleam's process.send_after uses erlang:send_after, which is implemented using a BIF/NIF. However, there's no similar BIF/NIF implementation of a send_interval function in the erlang module.

The Erlang implementation of send_interval is actually in the timer module, which has both timer:send_after and timer:send_interval. All the functions in timer are implemented using ordinary Erlang processes & ETS tables, not special BIFs/NIFs.

The timer processes can get overloaded and are less efficient than erlang:send_after, so there's a warning about using them for any application that sets/cancels lots of timers: http://erlang.org/doc/efficiency_guide/commoncaveats.html#id65681.

We could use a combination of erlang:send_after (for efficiency) and timer:send_interval (for convenience), but because the underlying implementations are completely different, the TimerRef returned by erlang:send_after is not compatible with the TRef returned by the functions in the timer module. This is mostly a problem if we cant to cancel timers. To cancel a timer set by the timer module, we need to use timer:cancel, whereas to cancel a timer set by erlang:send_after, we must use erlang:cancel_timer.

Other options I can think of:

  1. Use only erlang:send_after, and use it to implement a send_interval function on the Gleam side. I'm not sure how to do this.
  2. Create a non-core gleam_timer package that simply wraps the Erlang timer module, so users can just add it as a dependency if they want send_interval and aren't worried about the scalability issues.
  3. Don't implement a send_interval function at all for now; it's easy enough for Gleam users to wire up their processes to send repeated messages to themselves using the existing process.send_after function.
lpil commented 3 years ago

Great research, very thorough. I like your suggestions a lot. I think I'd like to go with 3 and leave the community to implement 2.

If it turns out there is an excellent community solution we could later promote it into a core package if that's desired, but a community package could be the best solution long term.