defmodule WaitForIt do @moduledoc ~S""" Provides various ways of waiting for things to happen. ## Overview Elixir is a functional programming language with an emphasis on immutability of data. However, when dealing with shared state or interacting with external systems, *change happens*. WaitForIt provides various ways of waiting for such changes to happen. While Elixir provides several language and standard library features (such as `Process.sleep/1`, `receive/1`/`after`, and `Task.async/1`/`Task.await/2`) that can be used to implement waiting, they are inconvenient to use for this purpose. WaitForIt builds on top of these language features to provide convenient and easy-to-use facilities for waiting on specific conditions. While this is likely most useful for test code in which tests must wait for concurrent or asynchronous activities to complete, it is also useful in any scenario where concurrent processes must coordinate their activity. Examples include asynchronous event handling, producer-consumer processes, and time-based activity. This page doubles as the API reference and an introduction. If you are just getting started, the task-focused guides walk through the most common scenarios: [Waiting in tests](waiting_in_tests.html), [Polling vs signaling](polling_vs_signaling.html), [Composing waits](composing_waits.html), [Recipes](recipes.html), and [Telemetry](telemetry.html). ## Quick start To use WaitForIt, you must first `require WaitForIt` or `import WaitForIt`. There are five distinct forms of waiting provided. Jump to the docs for each for more information. #### wait The `wait/2` macro waits until a given expression evaluates to a truthy value. # Wait up to one minute for a file to exist, and then print its contents if WaitForIt.wait(File.exists?("data.csv"), timeout: :timer.minutes(1)) do IO.puts(File.read!("data.csv")) else IO.warn("Stopped waiting for the file to exist") end #### match_wait The `match_wait/3` macro waits until a given expression matches a given pattern, and binds out of it. It is the most convenient form when waiting for a tagged result such as `{:ok, value}`. # Wait up to two seconds for a record to exist, binding it directly. {:ok, user} = WaitForIt.match_wait({:ok, %User{}}, Repo.fetch(User, id), timeout: 2_000) #### case_wait The `case_wait/3` macro waits until a given expression evaluates to a value that matches any one of the given case clauses. It looks and acts like an Elixir `case/2` expression except that it can take an optional `else` clause. # Wait for 30 seconds for a directory to exist, and then write a file in it WaitForIt.case_wait(File.stat("data"), timeout: :timer.seconds(30)) do {:ok, %File.Stat{type: :directory}} -> File.write!("data/greeting.txt", "Hello, world!") else {:ok, %File.Stat{type: type}} -> IO.warn("Expected 'data' to be a directory but its type is #{inspect(type)}") {:error, reason} -> IO.warn("Could not stat 'data': #{inspect(reason)}") end #### cond_wait The `cond_wait/2` macro waits until any one of the given expressions evaluates to a truthy value. It looks and acts like an Elixir `cond/1` expression except that it can take an optional `else` clause. # Wait for up to one minute for either a specific file to exist OR for the top of the minute # to be reached. WaitForIt.cond_wait(timeout: :timer.seconds(10), interval: 500) do File.exists?("data/process.json") -> IO.puts("Processing...") NaiveDateTime.utc_now().second == 0 -> IO.puts("Processing...") else IO.warn("Stopped waiting since neither condition ever became truthy") end #### with_wait The `with_wait/3` macro composes several waits in a pipeline. It looks and acts like an Elixir `with/1` expression, except that its `<~` clauses *wait* until their expression matches. # Wait for an account to load, then for its balance, short-circuiting to else on timeout. WaitForIt.with_wait on( {:ok, account} <~ {load_account(token), timeout: 2_000}, {:ok, balance} <~ fetch_balance(account) ) do {:ok, balance} else not_ready -> {:error, {:timed_out, not_ready}} end ### Options All forms of waiting accept the same set of options to control their behavior: * `:timeout` - the amount of time to wait (in milliseconds) before giving up (default: `5_000`) * `:pre_wait` - wait for the given number of milliseconds before evaluating conditions for the first time (default: `0`) * `:interval` - the polling interval (in milliseconds) at which to re-evaluate conditions (default: `100`) * `:signal` - disable polling and use a signal of the given name instead > #### `:frequency` is now `:interval` {: .info} > > The `:frequency` option has been renamed to `:interval`, which more accurately describes a > time value in milliseconds. `:frequency` continues to work as an alias and is slated for > removal in a future major version. If both are given, `:interval` takes precedence. See [Polling-based waiting](#module-polling-based-waiting) for more information on the `:interval` option and [Signal-based waiting](#module-signal-based-waiting) for more information on the `:signal` option. ### Timeout behavior The forms of waiting differ in what happens when a wait times out. This table summarizes the behavior; the non-bang forms mirror the corresponding built-in Elixir construct, while every bang form raises a `WaitForIt.TimeoutError`. | Construct | On timeout (no `else`) | On timeout (with `else`) | Bang variant raises | | ------------------- | ---------------------------- | -------------------------- | ------------------- | | `wait/2` | returns the last falsy value | _(no `else` clause)_ | `TimeoutError` | | `match_wait/3` | raises `MatchError` | _(no `else` clause)_ | `TimeoutError` | | `case_wait/3` | raises `CaseClauseError` | evaluates `else` | `TimeoutError` | | `cond_wait/2` | raises `CondClauseError` | evaluates `else` | `TimeoutError` | | `with_wait/3` | returns the last value | evaluates `else` (a `<~` timeout flows here) | `TimeoutError` (`<~` clauses) | ## Waitable expressions and waiting conditions _Waitable expressions_ and _waiting conditions_ are fundamental concepts in WaitForIt. A _waitable expression_ is any arbitrary Elixir expression that can be evaluated one or more times to produce a value. A _waiting condition_ is a conditional expression that indicates whether waiting should continue or be halted with a particular value. In the case of `wait/2`, there is a single waitable expression, which is passed as the first argument of the macro, and an implicit waiting condition, which is based on the truthiness of the associated waitable expression. For example: WaitForIt.wait(2 + 2 == 5, timeout: 200) In this example the waitable expression is `2 + 2 == 5` and the implicit waiting condition is the truthiness of that expression. The waitable expression is repeatedly evaluated until the value that it produces satisfies the waiting condition. In this case, the value of evaluating the expression is always `false` so it will never satisfy the waiting condition of a truthy value, and will therefore result in a timeout. For `case_wait/3`, there is a single waitable expression and one or more explicit waiting conditions expressed as case clauses. For example: WaitForIt.case_wait(File.stat("data.csv"), timeout: :timer.seconds(10)) do {:ok, %File.Stat{} = file_stat} -> IO.inspect(file_stat) end In this example the waitable expression is `File.stat("data.csv")`, which, upon evaluation, results in a value of either `{:ok, %FileStat{}}` or `{:error, reason}`. There is also one explicit waiting condition, which is the case clause `{:ok, %File.Stat{} = file_stat}`. The waitable expression will be repeatedly evaluated until it produces a value satisfying the lone waiting condition. In other words, it will wait until the file exists or a timeout occurs. For `cond_wait/2`, there can be one or more waitable expressions and each one is paired with an implicit waiting condition, which is the truthiness of the waitable expression value. For example: WaitForIt.cond_wait(timeout: :timer.hours(1)) do Date.utc_today().day == 1 -> IO.puts("It's the first day of the month") NaiveDateTime.utc_now().minute == 30 -> IO.puts("It's half past the hour") end In this example, there are two waitable expressions: `Date.utc_today().day == 1` and `NaiveDateTime.utc_now().minute == 30`. Each of these is paired with an implicit waiting condition, which is the truthiness of the value produced by evaluating the expression. ### Idempotency of waitable expressions Waitable expressions are by their nature subject to change with repeated evaluations over time. Therefore, idempotent expressions are of little use in the context of waiting, since waiting would either halt immediately (if the expression already satisfies the waiting conditions) or never halt at all (if it does not satisfy the waiting conditions). For this reason, it is expected that the value produced by waitable expressions may change on each re-evaluation, and that it is possible for each re-evaluation to produce side-effects. It is important, however, that any side-effects that can occur during evaluation of the expression are safe and predictable, since the expression may be evaluated an indeterminate number of times while waiting. ## Polling-based waiting By default, WaitForIt uses a polling-based waiting mode in which waitable expressions are periodically re-evaluated until waiting conditions have been met or a timeout has occurred. The interval at which waitable expressions are evaluated can be controlled by the `:interval` option, which specifies the delay between evaluations in milliseconds and is supported by all forms of waiting. (The legacy alias `:frequency` is also accepted; see the Options section.) The `:interval` option may also be a 1-arity function of the attempt number, which enables backoff strategies — for example, polling less aggressively as time goes on so as not to hammer a struggling dependency. See `WaitForIt.Backoff` for ready-made strategies such as exponential backoff with jitter. ## Signal-based waiting Signal-based waiting obviates the need for polling by using a signaling mechanism to indicate that waiting conditions should be re-evaluated in response to some event. With signal-based waiting, instead of periodically re-evaluating conditions at a particular frequency, a signal is sent to waiters to indicate when waiting conditions should be re-evaluated. It is expected that the `signal/1` function will be used to unblock the waiting code in order to re-evaluate the waiting conditions. To use signal-based waiting instead of polling-based waiting use the `:signal` option that is supported by all forms of waiting. The value of the `:signal` option is an arbitrary term (typically an atom or a tuple of atoms) that serves as the binding between the waiting conditions and the asynchronous code that can alter the outcome of those waiting conditions. When the `:signal` option is used, WaitForIt will automatically wait until a matching signal is received and then re-evaluate waiting conditions. If the waiting conditions are satisfied then the wait is halted, if not then the wait continues until the next signal is received or a timeout occurs. By way of example, imagine a typical producer-consumer problem in which a consumer process waits for items to appear in some buffer while a separate producer process occasionally places items in the buffer. In this scenario, the consumer process might use the `wait/2` macro with the `:signal` option to wait until there are some items in the buffer and the producer process would use the `signal/1` function to tell the consumer that it might be time for it to check the buffer again. # CONSUMER process WaitForIt.wait Buffer.count() >= 4, signal: :wait_for_buffer # PRODUCER process # put some things in buffer, then signal waiters Buffer.put(1) Buffer.put(2) WaitForIt.signal(:wait_for_buffer) Notice that the same signal name, `:wait_for_buffer`, is used by both the consumer and the producer, which is what allows the producer to signal to the consumer that waiting conditions should be re-evaluated. It is important to realize that just because a signal has been emitted does not necessarily mean that any waiting conditions have been satisfied. Rather, a signal indicates that waiters should re-evaluate their waiting conditions to determine if they should continue to wait or not. See the [Polling vs signaling](polling_vs_signaling.html) guide for guidance on choosing between the two modes. ## Telemetry Every wait emits [`:telemetry`](https://hexdocs.pm/telemetry) events under the `[:wait_for_it, :wait]` prefix — `:start`, `:stop`, and `:exception` — so you can observe how long waits take, how many evaluations they require, and how often they time out. The `:stop` event reports the wait `duration`, the number of `evaluations`, and whether the wait `:matched` or hit a `:timeout`. See the [Telemetry guide](telemetry.html) for the full measurement and metadata reference, plus examples of attaching handlers and wiring up `Telemetry.Metrics`. ## Using WaitForIt in tests Tests — especially integration and end-to-end tests — are one of the most common places to wait on asynchronous work. The `WaitForIt.Test` module provides ExUnit assertions (`assert_eventually/2`, `refute_eventually/2`, and `assert_always/2`) that wait and re-evaluate and, on timeout, fail with a regular `ExUnit.AssertionError` that includes the source expression and the last value seen: defmodule MyApp.SomeTest do use ExUnit.Case use WaitForIt.Test test "the user is eventually confirmed" do assert_eventually {:ok, %User{confirmed: true}} = Repo.fetch(User, user_id) end end The waiting macros described above can also be used directly in tests when you want their exact return values or timeout semantics — `wait/2`, for example, returns its value and so drops straight into an `assert`. See the [Waiting in tests](waiting_in_tests.html) guide for a full walkthrough. ## A note on "catch-all" clauses It is common to include "catch-all" clauses in normal Elixir `case/2` and `cond/1` expressions. Often, a `case/2` expression will include a final catch-all clause (like `_`) which will always match. Similarly, a `cond/1` expression will typically include a final always-truthy condition (like `true`) which will always match. When using the waiting variants of these constructs, `case_wait/3` and `cond_wait/2`, it is *not* recommended to use such catch-all clauses. The reason for this is that, since catch-all clauses by definition always match, including one as a waiting condition would not allow for re-evaluating any other waiting conditions and would terminate the wait immediately after the first evaluation. Instead of using a catch-all clause that always matches, an `else` clause can be used instead. Unlike catch-all clauses, an `else` clause is only evaluated if there is a timeout during the associated waiting operation. Both `case_wait/3` and `cond_wait/2` support `else` clauses, which allows for customizing the behavior and return value of the expression in the event of a timeout. """ @typedoc """ Type to represent an expression that can be waited on. """ @type wait_expression :: Macro.t() @typedoc """ Options that can be used to control waiting behavior. """ @type wait_opt :: {:timeout, non_neg_integer()} | {:interval, non_neg_integer()} | {:frequency, non_neg_integer()} | {:pre_wait, non_neg_integer()} | {:signal, atom() | nil} @typedoc """ Options that can be used to control waiting behavior. See `t:wait_opt/0`. """ @type wait_opts :: [wait_opt()] @type signal :: atom() | {atom(), atom()} | {atom(), atom(), atom()} | list(atom()) | term() @doc ~S""" Wait until the given `expression` evaluates to a truthy value. Returns the truthy value that ended the wait, or the last falsy value evaluated if a timeout occurred. > #### Warning {: .warning} > > The value returned from this macro has changed as of version 2.0. > > In previous versions, `{:ok, value}` would be returned for the success case, and > `{:timeout, timeout_milliseconds}` would be returned for the timeout case. > > As of version 2.0, the final value of the wait expression is returned directly, which will > be a truthy value for the success case and a falsy value for the timeout case. This allows > the `wait/2` macro to be used in conditional expressions, such as in `if/2`/`else` expressions, > or in assertions in tests. > > If you are migrating from version 1.x and rely on the return value, you can enable the > previous behavior by using the `WaitForIt.V1.wait/2` macro instead. ## Options See the WaitForIt module documentation for further discussion of these options. * `:timeout` - the amount of time to wait (in milliseconds) before giving up * `:pre_wait` - wait for the given number of milliseconds before evaluating conditions for the first time * `:interval` - the polling interval in milliseconds, or a `WaitForIt.Backoff` function, at which to re-evaluate conditions (alias: `:frequency`) * `:signal` - disable polling and use a signal of the given name instead ## Examples Wait until the top of the hour: WaitForIt.wait Time.utc_now.minute == 0, frequency: 60_000, timeout: 60_000 * 60 Wait up to one minute for a particular record to appear in the database: if data = WaitForIt.wait Repo.get(Post, 42), frequency: 1000, timeout: :timer.seconds(60) do IO.inspect(data) else IO.puts("Gave up after #{timeout} milliseconds") end Assert that a database record is created by some asynchronous process: do_some_async_work() assert %Post{id: 42} = WaitForIt.wait Repo.get(Post, 42) """ @doc section: :wait defmacro wait(expression, opts \\ []) do quote do require WaitForIt.Waitable.BasicWait waitable = WaitForIt.Waitable.BasicWait.create(unquote(expression)) WaitForIt.Waiting.wait(waitable, unquote(opts), __ENV__) end end @doc """ The same as `wait/2` but raises a `WaitForIt.TimeoutError` exception if it fails. """ @doc section: :wait defmacro wait!(expression, opts \\ []) do quote do require WaitForIt.Waitable.BasicWait waitable = WaitForIt.Waitable.BasicWait.create(unquote(expression)) WaitForIt.Waiting.wait!(waitable, unquote(opts), __ENV__) end end @doc ~S""" Wait until the given `expression` matches one of the case clauses in the given block. Returns the value of the matching clause, or the value of the optional `else` clause in the event of a timeout. The `do` block passed to this macro must be a series of case clauses exactly like a built-in Elixir `case/2` expression. Just like a `case/2` expression, the clauses will attempt to be matched from top to bottom and the first one that matches will provide the resulting value of the expression. The difference with `case_wait/3` is that if none of the clauses initially matches it will wait and periodically re-evaluate the clauses until one of them does match or a timeout occurs. An optional `else` clause may also be used to provide the value in case of a timeout. If an `else` clause is provided and a timeout occurs, then the `else` clause will be evaluated and the resulting value of the `else` clause becomes the value of the `case_wait/3` expression. If no `else` clause is provided and a timeout occurs, then a `CaseClauseError` is raised, exactly as if a normal Elixir `case/2` expression were being used. The optional `else` clause may also take the form of match clauses, such as those in the `else` clause of a `with/1` expression. In this form, the `else` clause can match on the final value of the expression that was evaluated before the timeout occurred. See the examples below for an example of this. > #### Beware "catch-all" clauses {: .warning} > > `case_wait/3` expressions should *not* include a final "catch-all" clause, such as `_`, which > would always match. Instead, an `else` clause can be used to customize the behavior and > return value in the event of a waiting timeout. > > See [A note on "catch-all" clauses](#module-a-note-on-catch-all-clauses) in the module docs > for further information. ## Options See the WaitForIt module documentation for further discussion of these options. * `:timeout` - the amount of time to wait (in milliseconds) before giving up * `:pre_wait` - wait for the given number of milliseconds before evaluating conditions for the first time * `:interval` - the polling interval in milliseconds, or a `WaitForIt.Backoff` function, at which to re-evaluate conditions (alias: `:frequency`) * `:signal` - disable polling and use a signal of the given name instead ## Examples Wait until queue has at least 5 messages, then return them: WaitForIt.case_wait Queue.get_messages(queue), timeout: 30_000, frequency: 100 do messages when length(messages) > 4 -> messages else # If after 30 seconds we still don't have 5 messages, just return the messages we do have. messages -> messages end A thermostat that keeps temperature in a small range: def thermostat(desired_temperature) do WaitForIt.case_wait get_current_temperature() do temp when temp > desired_temperature + 2 -> turn_on_air_conditioning() temp when temp < desired_temperature - 2 -> turn_on_heat() end thermostat(desired_temperature) end Wait until the process mailbox is small enough before flooding it with more messages: WaitForIt.case_wait Process.info(stream_pid, :message_queue_len), frequency: 10, timeout: 60_000 do {:message_queue_len, len} when len < 500 -> send_chunk(conn, chunk) else len -> raise "Timeout while sending stream response. [message_queue_len: #{len}]" end > #### Production-ready {: .info} > > The above example is a real-world use of WaitForIt that was used to solve an issue with chunked > HTTP responses using [plug_cowboy](https://github.com/elixir-plug/plug_cowboy). The underlying > issue has since been fixed but this example is a good illustration of using WaitForIt to > solve a production problem. > > See https://github.com/elixir-plug/plug_cowboy/issues/10 for background and further details, > if interested. """ @doc section: :case_wait defmacro case_wait(expression, opts \\ [], blocks) do case_clauses = Keyword.get(blocks, :do) else_block = Keyword.get(blocks, :else) quote do require WaitForIt.Waitable.CaseWait waitable = WaitForIt.Waitable.CaseWait.create( unquote(expression), unquote(case_clauses), unquote(else_block) ) WaitForIt.Waiting.wait(waitable, unquote(opts), __ENV__) end end @doc """ The same as `case_wait/3` but raises a `WaitForIt.TimeoutError` exception if it fails. """ @doc section: :case_wait defmacro case_wait!(expression, opts \\ [], blocks) do case_clauses = Keyword.get(blocks, :do) else_block = Keyword.get(blocks, :else) quote do require WaitForIt.Waitable.CaseWait waitable = WaitForIt.Waitable.CaseWait.create( unquote(expression), unquote(case_clauses), unquote(else_block) ) WaitForIt.Waiting.wait!(waitable, unquote(opts), __ENV__) end end @doc ~S""" Wait until one of the expressions in the given block evaluates to a truthy value. Returns the value corresponding with the matching expression, or the value of the optional `else` clause in the event of a timeout. The `do` block passed to this macro must be a series of expressions exactly like a built-in Elixir `cond/1` expression. Just like a `cond/1` expression, the embedded expresions will be evaluated from top to bottom and the first one that is truthy will provide the resulting value of the expression. The difference with `cond_wait/2` is that if none of the expressions is initially truthy it will wait and periodically re-evaluate them until one of them becomes truthy or a timeout occurs. An optional `else` clause may also be used to provide the value in case of a timeout. If an `else` clause is provided and a timeout occurs, then the `else` clause will be evaluated and the resulting value of the `else` clause becomes the value of the `cond_wait/2` expression. If no `else` clause is provided and a timeout occurs, then a `CondClauseError` is raised, exactly as if a normal Elixir `cond/1` expression were being used. > #### Beware "catch-all" clauses {: .warning} > > `cond_wait/2` expressions should *not* include a final "catch-all" clause, such as `true`, > which would always match. Instead, an `else` clause can be used to customize the behavior and > return value in the event of a waiting timeout. > > See [A note on "catch-all" clauses](#module-a-note-on-catch-all-clauses) in the module docs > for further information. ## Options See the WaitForIt module documentation for further discussion of these options. * `:timeout` - the amount of time to wait (in milliseconds) before giving up * `:pre_wait` - wait for the given number of milliseconds before evaluating conditions for the first time * `:interval` - the polling interval in milliseconds, or a `WaitForIt.Backoff` function, at which to re-evaluate conditions (alias: `:frequency`) * `:signal` - disable polling and use a signal of the given name instead ## Examples Trigger an alarm when any sensors go beyond a threshold: def sound_the_alarm do WaitForIt.cond_wait timeout: 60_000 * 60 * 24 do read_sensor(:sensor1) > 9 -> IO.puts("Alarm: :sensor1 too high!") read_sensor(:sensor2) < 100 -> IO.puts("Alarm: :sensor2 too low!") read_sensor(:sensor3) < 0 -> IO.puts("Alarm: :sensor3 below zero!") else IO.puts("All is good...for now.") end # Recursively call to wait for the next sensor readings... sound_the_alarm() end """ @doc section: :cond_wait defmacro cond_wait(opts \\ [], blocks) do cond_clauses = Keyword.get(blocks, :do) else_block = Keyword.get(blocks, :else) quote do require WaitForIt.Waitable.CondWait waitable = WaitForIt.Waitable.CondWait.create( unquote(cond_clauses), unquote(else_block) ) WaitForIt.Waiting.wait(waitable, unquote(opts), __ENV__) end end @doc """ The same as `cond_wait/2` but raises a `WaitForIt.TimeoutError` exception if it fails. """ @doc section: :cond_wait defmacro cond_wait!(opts \\ [], blocks) do cond_clauses = Keyword.get(blocks, :do) else_block = Keyword.get(blocks, :else) quote do require WaitForIt.Waitable.CondWait waitable = WaitForIt.Waitable.CondWait.create( unquote(cond_clauses), unquote(else_block) ) WaitForIt.Waiting.wait!(waitable, unquote(opts), __ENV__) end end @doc ~S""" Wait until the given `expression` matches the given `pattern`. Returns the value that the expression evaluated to when it matched the pattern. The `pattern` may include a guard, exactly like the left-hand side of a `case/2` clause or a `<-` clause in `with/1`. Where `wait/2` waits for a *truthy* value, `match_wait/3` waits for a value that *matches a pattern* and binds out of it. It is the most convenient form when you are waiting for a tagged result such as `{:ok, value}` and want `value` directly. > #### Beware bound variables in the pattern {: .warning} > > Just like `case/2`, variables in the pattern are *binding* unless pinned with `^`. The > pattern is only a waiting condition, not an assertion about a specific value, so prefer > guards (`when ...`) to express conditions on the matched value. ## Options See the `WaitForIt` module documentation for further discussion of these options. * `:timeout` - the amount of time to wait (in milliseconds) before giving up * `:pre_wait` - wait for the given number of milliseconds before evaluating conditions for the first time * `:interval` - the polling interval (in milliseconds) at which to re-evaluate conditions * `:signal` - disable polling and use a signal of the given name instead ## Examples Wait until a database record exists and bind it directly: {:ok, user} = match_wait({:ok, %User{}}, Repo.fetch(User, id), timeout: 2_000) Wait until a value satisfies a guard, then use the bound variable: count = match_wait(n when n > 99, get_counter(), signal: :counter_wait) On timeout (with no matching value), a `MatchError` is raised, exactly as if a normal pattern match had failed. Use `match_wait!/3` to raise a `WaitForIt.TimeoutError` instead. """ @doc section: :match_wait defmacro match_wait(pattern, expression, opts \\ []) do quote do require WaitForIt.Waitable.MatchWait waitable = WaitForIt.Waitable.MatchWait.create(unquote(pattern), unquote(expression)) WaitForIt.Waiting.wait(waitable, unquote(opts), __ENV__) end end @doc """ The same as `match_wait/3` but raises a `WaitForIt.TimeoutError` exception on timeout. """ @doc section: :match_wait defmacro match_wait!(pattern, expression, opts \\ []) do quote do require WaitForIt.Waitable.MatchWait waitable = WaitForIt.Waitable.MatchWait.create(unquote(pattern), unquote(expression)) WaitForIt.Waiting.wait!(waitable, unquote(opts), __ENV__) end end @doc ~S""" Compose several waits in a `with`-style pipeline. `with_wait` looks and behaves like an Elixir `with/1` expression, except that its clauses can *wait*. The clauses are wrapped in `on(...)`, and each clause is one of: * `pattern <- expression` - an ordinary `with` clause, evaluated once. If it matches, bind and continue; otherwise route to the `else` block. * `pattern <~ expression` - a *wait-for-match* clause: re-evaluate `expression` until it matches `pattern`, then bind and continue. * `pattern <~ {expression, opts}` - a wait-for-match clause with per-clause options (such as `timeout:`/`interval:`/`signal:`) that override the global options. Global options for every `<~` clause may be given between the `on(...)` wrapper and the block: `with_wait on(...), timeout: 2_000 do ... end`. On success, the `do` block is evaluated and its value returned. If any clause fails to match — including a `<~` clause that *times out* — control transfers to the `else` block (or, if there is no `else`, the non-matching value becomes the result), exactly like `with/1`. A `<~` timeout is therefore indistinguishable from an ordinary non-match: the last evaluated value flows to `else`. > #### `<~` precedence {: .warning} > > `<~` binds more tightly than `when` and the comparison operators, so guards and right-hand > sides that use those operators must be parenthesized: > > ({:ok, n} when n > 5) <~ poll() > found <~ (Enum.find(items, &ready?/1) != nil) > > Simple clauses such as `{:ok, x} <~ fetch(id)` and `{:ok, x} <~ {fetch(id), timeout: 100}` > need no parentheses. For a wait dominated by a single complex condition, prefer `case_wait/3`. ## Examples with_wait on( {:ok, token} <- authenticate(user), {:ok, account} <~ {load_account(token), timeout: 2_000}, {:ok, balance} <~ fetch_balance(account) ), interval: 50 do {:ok, balance} else {:error, reason} -> {:error, reason} still_pending -> {:still_waiting, still_pending} end """ @doc section: :with_wait defmacro with_wait(clauses, opts \\ [], blocks) do WaitForIt.WithWait.build(clauses, opts, blocks, :return_last_value) end @doc """ The same as `with_wait/3` but a `<~` clause that times out raises a `WaitForIt.TimeoutError` instead of routing to the `else` block. An `else` block is still honored for ordinary `<-` clauses that do not match. """ @doc section: :with_wait defmacro with_wait!(clauses, opts \\ [], blocks) do WaitForIt.WithWait.build(clauses, opts, blocks, :raise) end @doc false # Internal building block for `with_wait` clauses: waits until `expression` matches `pattern`, # returning the matched value. The timeout behavior is controlled by the `:on_timeout` option # carried in `opts` (`:return_last_value` for `with_wait`, `:raise` for `with_wait!`). defmacro __wait_clause__(pattern, expression, opts) do match_pattern = WaitForIt.Evaluation.ignore_pattern_bindings(pattern) quote do require WaitForIt.Waitable.MatchWait waitable = WaitForIt.Waitable.MatchWait.create(unquote(match_pattern), unquote(expression)) WaitForIt.Waiting.wait(waitable, unquote(opts), __ENV__) end end @doc """ Send a signal to indicate that any processes waiting on the signal should re-evaluate their waiting conditions. """ @doc section: :signal @spec signal(signal()) :: :ok def signal(signal) do Registry.dispatch(WaitForIt.SignalRegistry, signal, fn waiters -> for {pid, _env} <- waiters, do: send(pid, {:wait_for_it_signal, signal}) end) end end