ExWaiter (ex_waiter v0.7.0) View Source

Handy functions for polling and receiving.

  • Polling: poll/2 and poll!/2 periodically check that a given condition has been met.
  • Receiving: receive/2 and receive!/2 return the next message/s from the mailbox within a timeout.

Installation

Add the latest release to your mix.exs file:

defp deps do
  [
    {:ex_waiter, "~> 0.7.0"}
  ]
end

Then run mix deps.get in your shell to fetch the dependencies.

Link to this section Summary

Functions

Periodically checks that a given condition has been met.

Periodically checks that a given condition has been met. Raises an exception upon exhausted retries.

Returns the next message/s from the mailbox within a timeout.

Returns the next message/s from the mailbox within a timeout. Raises an exception upon timeout.

Link to this section Types

Specs

poll_options() ::
  {:delay, ExWaiter.Polling.Poller.delay()}
  | {:num_attempts, ExWaiter.Polling.Poller.num_attempts()}
  | {:on_complete, ExWaiter.Polling.Poller.on_complete()}

Specs

receive_options() ::
  {:timeout, timeout()}
  | {:filter, ExWaiter.Receiving.Receiver.filter_fn()}
  | {:on_complete, ExWaiter.Receiving.Receiver.on_complete()}

Link to this section Functions

Link to this function

poll(polling_fn, opts \\ [])

View Source

Specs

poll(ExWaiter.Polling.Poller.polling_fn(), [poll_options()]) ::
  :ok | {:ok, any()} | :error | {:error, any()}

Periodically checks that a given condition has been met.

In some scenarios there is no obvious way to ensure that asynchronous side effects have taken place without continuously checking for successful completion. For example, perhaps an assertion is needed on click data being asynchronously persisted to the database. It is not difficult to write a recursive function to handle this one-off, but there is a bit of ceremony involved. Additionally, perhaps it is desirable to configure the amount of delay prior to each check, the total number of attempts, and a record of the history of each attempt.

Usage

Takes a function that checks whether the given condition has been met. This function can take 0 or 1 arguments, with the argument being the %Poller{}. Returning {:ok, value} or {:error, value} will ensure that you receive a return "value" from poll/2 and that value changes are tracked throughout attempts. If the value doesn't matter, :ok and :error may be returned from the function instead. If the condition has been met, a tuple with {:ok, value} (or :ok) will be returned. If retries are exhausted prior to the condition being met, {:error, value} (or :error) will be returned.

Options

  • :num_attempts - The number of attempts before retries are exhausted. Takes either an integer or :infinity. (default: 5)
  • :delay - Takes either an integer or a function that receives the %Poller{} struct at that moment and returns a number of milliseconds to delay prior to performing the next attempt. The default is fn poller -> poller.attempt_num * 10 end.
  • :on_complete - Configures a callback upon the condition being met or retries being exhausted. Takes a function that receives the %Poller{} struct. Can be used for logging and inspection.

Examples

By default, this will attempt this query up to 5 times in 100ms.

assert {:ok, %Project{}} = ExWaiter.poll(fn ->
  case Projects.get(1) do
    %Project{} = project -> {:ok, project}
    _ -> :error
  end
end)

The number of attempts and delay between each can be configured.

assert {:ok, %Project{}} = ExWaiter.poll(fn ->
  case Projects.get(1) do
    %Project{} = project -> {:ok, project}
    _ -> :error
  end,
  num_attempts: 10,
  delay: 20
end)

A callback upon the condition being met or retries being exhausted can be configured. This callback receives the %Poller{} for inspection.

assert {:ok, %Project{}} = ExWaiter.poll(fn ->
  case Projects.get(1) do
    %Project{} = project -> {:ok, project}
    _ -> :error
  end,
  on_complete: fn poller ->
    assert %{
      attempt_num: 5,
      num_attempts: 5,
      attempts: [
        %{value: :error, delay: 10},
        %{value: :error, delay: 20},
        %{value: :error, delay: 30},
        %{value: :error, delay: 40},
        %{value: {:ok, %Project{}}, delay: 50}
      ],
      total_delay: 100,
      value: {:ok, %Project{}}
    }
  end
end)

The poller function can optionally receive an argument, which will be the Poller struct at that moment. This can be used for customization and logging.

assert {:ok, {%Project{}, 5}} = ExWaiter.poll(fn poller ->
  case Projects.get(1) do
    %Project{} = project -> {:ok, {project, poller.attempt_num}}
    _ -> :error
  end
end)
Link to this function

poll!(polling_fn, opts \\ [])

View Source

Specs

poll!(Waiter.polling_fn(), [poll_options()]) :: any()

Periodically checks that a given condition has been met. Raises an exception upon exhausted retries.

Supports the same options as poll/2. However, if the condition has been met, only the "value" will be returned. If retries are exhausted prior to the condition being met, an exception will be raised.

Link to this function

receive(num_messages \\ 1, opts \\ [])

View Source

Specs

receive(pos_integer(), [receive_options()]) ::
  {:ok, any()} | {:ok, [any()]} | {:error, any()} | {:error, [any()]}

Returns the next message/s from the mailbox within a timeout.

Especially in testing scenarios, it can be useful to be able to assert that a number of messages are received in a mailbox in a specific order and that all of those messages are received within a timeout. It is not difficult to use receive to grab the messages, but there is a bit of ceremony/verbosity involved especially if requiring that all messages are received in a specific total amount of time.

Usage

By default, the next single message in the mailbox will be returned if it appears within 100ms. The number of messages to return and timeout are configurable. If the message/s are received within the timeout window, {:ok, message} will be returned for a single message or {:ok, [messages]} for multiple. If the configured timeout is reached prior to returning a single requested message, :error will be returned. If multiple messages were requested, {:error, [messages]} will be returned containing any messages that were received.

Options

  • :timeout - The time to wait for the number of messages requested from the mailbox. Takes either an integer (ms) or :infinity. (default: 100)
  • :filter - Configures which messages to return. This can be useful if a mailbox in testing is receiving a lot of extra messages that you don't care about. Takes a function that receives a new message and must return a boolean (true for a match). The default is fn _message -> true end. (i.e. match all messages). Please note that any rejected messages will be captured and tracked alongside the matched messages.
  • :on_complete - Configures a callback upon receiving all messages or timeout. Takes a function that receives the %Receiver{} struct. Can be used for logging and inspection.

Examples

By default, the next message in the mailbox is returned if it appears within 100ms.

send(self(), :hello)

assert {:ok, :hello} = ExWaiter.receive()

Multiple messages may be returned.

send(self(), :hello)
send(self(), :hi)
send(self(), :yo)

assert {:ok, [:hello, :hi]} = ExWaiter.receive(2)

A timeout (in ms) can be set. If the timeout occurs prior to receiving all requested messages, the messages that were received will be returned in the error tuple.

send(self(), :hello)
send(self(), :hi)
Process.send_after(self(), :yo, 80)

assert {:error, [:hello, :hi]} = ExWaiter.receive(3, timeout: 50)

Messages can be filtered to ignore noise in the mailbox.

send(self(), {:greeting, :hello})
send(self(), {:age, 25})
send(self(), {:greeting, :hi})

assert {:ok, [{:greeting, :hello}, {:greeting, :hi}]} =
          ExWaiter.receive(2, filter: &match?({:greeting, _}, &1))

A callback upon receiving all message or timeout can be configured. This callback receives the %Receiver{} for inspection.

send(self(), :hello)
Process.send_after(self(), :hi, 90)

{:ok, [:hello, :hi]} =
  ExWaiter.receive(2,
    on_complete: fn receiver ->
      assert %ExWaiter.Receiving.Receiver{
                message_num: 2,
                all_messages: [:hello, :hi],
                filtered_messages: [:hello, :hi],
                rejected_messages: [],
                num_messages: 2,
                remaining_timeout: 10,
                timeout: 100
              } = receiver
    end
  )
Link to this function

receive!(num_messages \\ 1, opts \\ [])

View Source

Specs

receive!(pos_integer(), [receive_options()]) :: any()

Returns the next message/s from the mailbox within a timeout. Raises an exception upon timeout.

Supports the same options as receive/1. However, if the mailbox has the right number of messages, only the message/s will be returned. If the messages are not received prior to the timeout, an exception will be raised.