AsyncWith (async_with v0.3.0) View Source
The asynchronous version of Elixir's with
.
async with
always executes the right side of each clause inside a new task.
Tasks are spawned as soon as all the tasks that it depends on are resolved.
In other words, async with
resolves the dependency graph and executes all
the clauses in the most performant way possible. It also ensures that, if a
clause does not match, any running task is shut down.
Example
defmodule AcmeWeb.PostController do
use AcmeWeb, :controller
use AsyncWith
def show(conn, %{"id" => id}) do
async with {:ok, post} <- Blog.get_post(id),
{:ok, author} <- Users.get_user(post.author_id),
{:ok, posts_by_the_same_author} <- Blog.get_posts(author),
{:ok, similar_posts} <- Blog.get_similar_posts(post),
{:ok, comments} <- Blog.list_comments(post),
{:ok, comments} <- Blog.preload(comments, :author) do
conn
|> assign(:post, post)
|> assign(:author, author)
|> assign(:posts_by_the_same_author, posts_by_the_same_author)
|> assign(:similar_posts, similar_posts)
|> assign(:comments, comments)
|> render("show.html")
end
end
end
Timeout attribute
The attribute @async_with_timeout
can be used to configure the maximum time
allowed to execute all the clauses. It expects a timeout in milliseconds, with
the default value of 5000
.
defmodule Acme do
use AsyncWith
@async_with_timeout 1_000
def get_user_info(user_id) do
async with {:ok, user} <- HTTP.get("users/#{user_id}"),
{:ok, stats} <- HTTP.get("users/#{user_id}/stats")
Map.merge(user, %{stats: stats})
end
end
end
Link to this section Summary
Functions
Used to combine matching clauses, executing them asynchronously.
Link to this section Functions
Used to combine matching clauses, executing them asynchronously.
async with
always executes the right side of each clause inside a new task.
Tasks are spawned as soon as all the tasks that it depends on are resolved.
In other words, async with
resolves the dependency graph and executes all
the clauses in the most performant way possible. It also ensures that, if a
clause does not match, any running task is shut down.
Let's start with an example:
iex> opts = %{width: 10, height: 15}
iex> async with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{:ok, 150}
As in with/1
, if all clauses match, the do
block is executed, returning its
result. Otherwise the chain is aborted and the non-matched value is returned:
iex> opts = %{width: 10}
iex> async with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
:error
In addition, guards can be used in patterns as well:
iex> users = %{"melany" => "guest", "ed" => :admin}
iex> async with {:ok, role} when is_atom(role) <- Map.fetch(users, "ed") do
...> :ok
...> end
:ok
Variables bound inside async with
won't leak; "bare expressions" may also
be inserted between the clauses:
iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> async with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil
An else
option can be given to modify what is being returned from
async with
in the case of a failed match:
iex> opts = %{width: 10}
iex> async with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...> end
{:error, :wrong_data}
If an else
block is used and there are no matching clauses, an
AsyncWith.ClauseError
exception is raised.
Order-dependent clauses that do not express their dependency via their used or defined variables could lead to race conditions, as they are executed in separated tasks:
async with Agent.update(agent, fn _ -> 1 end),
Agent.update(agent, fn _ -> 2 end) do
Agent.get(agent, fn state -> state end) # 1 or 2
end