Skip to content

Notifier

Problem

You need to specialize an actor so that at certain times during its life-cycle, you can take an appropriate action. For example, all TCP connections are fundamentally the same but, what they do at certain points like:

  • when opened
  • sending data
  • receiving data
  • when closing

will need to change based on “the type” of TCP connection in question.

Actor specialization arises often when you want to create reusable Pony code. There’s a pattern in the standard library that is one of the more common ways to address. Let’s have a look at the “Notifier pattern”.

Solution

interface TimerNotify
  """
  Notifications for timer.
  """
  fun ref apply(timer: Timer, count: U64): Bool =>
    """
    Called with the the number of times the timer has fired since this was last
    called. Usually, the value of `count` will be 1. If it is not 1, it means
    that the timer isn't firing on schedule.

    For example, if your timer is set to fire every 10 milliseconds, and
    `count` is 2, that means it has been between 20-29 milliseconds since the
    last time your timer fired. Non 1 values for a timer are rare and indicate
    a system under heavy load.

    Return true to reschedule the timer (if it has an interval), or
    false to cancel the timer (even if it has an interval).
    """
    true

  fun ref cancel(timer: Timer) =>
    """
    Called if the timer is cancelled. This is also called if the notifier
    returns false from its `apply` method.
    """
    None

And our corresponding actor:

use "collections"

class Timer
  """
  The `Timer` class represents a timer that fires after an expiration
  time, and then fires at an interval. When a `Timer` fires, it calls
  the `apply` method of the `TimerNotify` object that was passed to it
  when it was created.
  """

  var _expiration: U64
  var _interval: U64
  let _notify: TimerNotify
  embed _node: ListNode[Timer]

  new iso create(
    notify: TimerNotify iso,
    expiration: U64,
    interval: U64 = 0)
  =>
    """
    Create a new timer. The expiration time should be a nanosecond count
    until the first expiration. The interval should also be in nanoseconds.
    """
    _expiration = expiration + Time.nanos()
    _interval = interval
    _notify = consume notify
    _node = ListNode[Timer]
    try _node()? = this end

  fun ref _cancel() =>
    """
    Remove the timer from any list.
    """
    _node.remove()
    _notify.cancel(this)

  fun ref _fire(current: U64): Bool =>
    """
    A timer is fired if its expiration time is in the past. The notifier is
    called with a count based on the elapsed time since expiration and the
    timer interval. The expiration time is set to the next expiration. Returns
    true if the timer should be rescheduled, false otherwise.
    """
    let elapsed = current - _expiration

    if elapsed < (1 << 63) then
      let count = (elapsed / _interval) + 1
      _expiration = _expiration + (count * _interval)

      if not _notify(this, count) then
        _notify.cancel(this)
        return false
      end
    end

    (_interval > 0) or ((_expiration - current) < (1 << 63))

Discussion

It’s important to note that the notifier pattern is fundamentally one that involves callbacks. And that in particular, it is about specializing actors.

As seen in the Timer example from the standard library, all “reusable” logic is part of the actor. The points where a user would want to specialize what happens, involve calling methods on the supplied “notify” object.

For example, from our example above:

      if not _notify(this, count) then

Each time the Timer fires, we call the apply method of the _notify object allowing situation-specific code to be executed. In the case of the TimerNotify API, a boolean is returned. If the return value is false, the timer isn’t rearmed and our other callback is called:

        _notify.cancel(this)

What does this look like to a user? Here’s an example:

use "time"

actor Main
  new create(env: Env) =>
    let timers = Timers
    let timer = Timer(Notify(env), 5_000_000_000, 2_000_000_000)
    timers(consume timer)

class Notify is TimerNotify
  let _env: Env
  var _counter: U32 = 0

  new iso create(env: Env) =>
    _env = env

  fun ref apply(timer: Timer, count: U64): Bool =>
    _env.out.print(_counter.string())
    _counter = _counter + 1
    true

There’s a couple of key points that our example highlights about our implementation that weren’t clear before.

First, a notifier should always be an iso. Why? Because the notify object will probably need to keep and update state. If we were to supply the same notify to multiple Timer actors, the state would become shared and be unsafe across actors. In the Timer implementation you will see this in create taking a TimerNotify iso

  new iso create(
    notify: TimerNotify iso,
    expiration: U64,
    interval: U64 = 0)

And in our concrete Notify implementation where we default the reference type when creating a new Notify to iso:

  new iso create(env: Env) =>
    _env = env

The notifier pattern is incredibly powerful. It makes it very easy for programmers to easily plug-in to existing functionality and get up and running. In the majority of specialization cases, the notifier pattern is probably what you want. However, there is one drawback that you need to be aware of for more advanced cases.

Notifiers cannot receive messages. They can send messages to actors they hold references to, but they have no way to receive arbitrary messages. Access to the notifier is completely controlled by the encompassing actor. As limitations go, this usually isn’t a problem. However, it can be problematic for some more advanced use-cases. If you find yourself needing to send messages to a notifier, we suggest you take a look at the Mixin pattern.