View Source Buffy.Throttle behaviour (Buffy v2.3.0)

The Buffy.Throttle module will wait for a specified amount of time before invoking the function. If the function is called again before the time has elapsed, it's a no-op. Once the timer has expired, the function will be called, and any subsequent calls will start a new timer.

call     call   call               call           call
 | call   | call | call             | call         |
 |  |     |  |   |  |               |  |           |
┌─────────┐  ┌─────────┐            ┌─────────┐    ┌─────────┐
│ Timer 1 │  │ Timer 2 │            │ Timer 3 │    │ Timer 4 │
└─────────|  └─────────┘            └─────────┘    └─────────┘
          |            |                      |              |
          |            |                      |    Forth function invocation
          |            |            Third function invocation
          | Second function invocation
First function invocation

Example Usage

You'll first need to create a module that will be used to throttle.

defmodule MyTask do
  use Buffy.Throttle,
    throttle: :timer.minutes(2)

  def handle_throttle(args) do
    # Do something with args
  end
end

Next, you can use the throttle/1 function with the registered module.

iex> MyTask.throttle(args)
:ok

Options

  • :jitter (integer) - Optional. The amount of jitter or randomosity to add to the throttle function handle. This value is in milliseconds. Defaults to 0.

  • :registry_module (atom) - Optional. A module that implements the Registry behaviour. If you are running in a distributed instance, you can set this value to Horde.Registry. Defaults to Registry.

  • :registry_name (atom) - Optional. The name of the registry to use. Defaults to the built in Buffy registry, but if you are running in a distributed instance you can set this value to a named Horde.Registry process. Defaults to Buffy.Registry.

  • :restart (:permanent | :temporary | :transient) - Optional. The restart strategy to use for the GenServer. Defaults to :temporary.

  • :supervisor_module (atom) - Optional. A module that implements the DynamicSupervisor behaviour. If you are running in a distributed instance, you can set this value to Horde.DynamicSupervisor. Defaults to DynamicSupervisor.

  • :supervisor_name (atom) - Optional. The name of the dynamic supervisor to use. Defaults to the built in Buffy dynamic supervisor, but if you are running in a distributed instance you can set this value to a named Horde.DynamicSupervisor process. Defaults to Buffy.DynamicSupervisor.

  • :throttle (non_neg_integer) - Optional. The minimum amount of time to wait before invoking the function. This value is in milliseconds. The actual run time could be longer than this value based on the :jitter option.

Dynamic Options

Sometimes you want a different throttle value or jitter value based on the arguments you pass in. To deal with this, there are optional functions you can implement in your throttle module. These functions take in the arguments and will return the throttle and jitter values. For example:

defmodule MyThrottler do
  use Buffy.Throttle,
    registry_module: Horde.Registry,
    registry_name: MyApp.HordeRegistry,
    supervisor_module: Horde.DynamicSupervisor,
    supervisor_name: MyApp.HordeDynamicSupervisor,
    throttle: :timer.minutes(2)

  def get_jitter(args) do
    case args do
      %Cat{} -> :timer.minutes(2)
      %Dog{} -> :timer.seconds(10)
      _ -> 0
    end
  end
end

Using with Horde

If you are running Elixir in a cluster, you can utilize Horde to only run one of your throttled functions at a time. To do this, you'll need to set the :registry_module and :supervisor_module options to Horde.Registry and Horde.DynamicSupervisor respectively. You'll also need to set the :registry_name and :supervisor_name options to the name of the Horde registry and dynamic supervisor you want to use.

  defmodule MyThrottler do
    use Buffy.Throttle,
      registry_module: Horde.Registry,
      registry_name: MyApp.HordeRegistry,
      supervisor_module: Horde.DynamicSupervisor,
      supervisor_name: MyApp.HordeDynamicSupervisor,
      throttle: :timer.minutes(2)

    def handle_throttle(args) do
      # Do something with args
    end
  end

Telemetry

These are the events that are called by the Buffy.Throttle module:

  • [:buffy, :throttle, :throttle] - Emitted when the throttle/1 function is called.
  • [:buffy, :throttle, :handle, :jitter] - Emitted before the handle_throttle/1 function is called with the amount of jitter added to the throttle.
  • [:buffy, :throttle, :handle, :start] - Emitted at the start of the handle_throttle/1 function.
  • [:buffy, :throttle, :handle, :stop] - Emitted at the end of the handle_throttle/1 function.
  • [:buffy, :throttle, :handle, :exception] - Emitted when an error is raised in the handle_throttle/1 function.

All of these events will have the following metadata:

  • :args - The arguments passed to the throttle/1 function.
  • :key - A hash of the passed arguments used to deduplicate the throttled function.
  • :module - The module using Buffy.Throttle.

With the additional metadata for [:buffy, :throttle, :handle, :stop]:

  • :result - The return value of the handle_throttle/1 function.

Memory Leaks

With any sort of debounce and Elixir processes, you need to be careful about handling too many processes, or having to much state in memory at the same time. If you handle large amounts of data there is a good chance you'll end up with high memory usage and possibly affect other parts of your system.

To help monitor this usage, Buffy has a telemetry metric that measures the Elixir process memory usage. If you summarize this metric you should get a good view into your buffy throttle processes.

summary("buffy.throttle.total_heap_size", tags: [:module])

Summary

Types

A list of arbitrary arguments that are used for the handle_throttle/1 function.

A unique key for debouncing. This is used for GenServer uniqueness and is generated from hashing all of the args.

Internal state that Buffy.Throttle keeps.

Callbacks

Returns the amount of jitter in milliseconds to add to the throttle time.

Returns the amount of throttle time in milliseconds.

The function called after the throttle has completed. This function will receive the arguments passed to the throttle/1 function.

A function to call the throttle. This will start and wait the configured throttle time before calling the handle_throttle/1 function.

Types

@type args() :: term()

A list of arbitrary arguments that are used for the handle_throttle/1 function.

@type key() :: term()

A unique key for debouncing. This is used for GenServer uniqueness and is generated from hashing all of the args.

@type state() :: {key(), args()}

Internal state that Buffy.Throttle keeps.

Callbacks

@callback get_jitter(args()) :: non_neg_integer()

Returns the amount of jitter in milliseconds to add to the throttle time.

@callback get_throttle(args()) :: non_neg_integer()

Returns the amount of throttle time in milliseconds.

@callback handle_throttle(args()) :: any()

The function called after the throttle has completed. This function will receive the arguments passed to the throttle/1 function.

@callback throttle(args()) :: :ok | {:error, term()}

A function to call the throttle. This will start and wait the configured throttle time before calling the handle_throttle/1 function.