dantswain

dantswain

LiveView asynchronous task patterns

Hi! I was wondering what patterns people have used for the following scenario.

  • When the page first loads, spawn some (potentially longer-running, e.g., fetching data from an external API) task.
  • When that task finishes, update the UI.

What I have after a couple iterations is something like this:

# do this in handle_params because I want live_patch to trigger it
def handle_params(params, _uri, socket) do
  if connected?(socket) do
    view_pid = self()
    spawn(fn -> 
      result = make_slow_api_call(params) 
      send(view_pid, {:api_call_done, result})
    end)
  end
  {:noreply, socket}
end

def handle_info({:api_call_done, result}, socket) do
  {:noreply, assign(socket, api_result: result)}
end

defp make_slow_api_call(...) do
  # ...
end

This works OK, however there are a couple shortcomings that I would love to handle better:

  • It’s hard to test for multiple reasons.
    • make_slow_api_call is private, so the only way to trigger it is to render the view.
    • Whenever I render the page, I need to wait for make_slow_api_call to finish, else I have stray processes after the test finishes. Doing this is not trivial and is error prone.
  • Handling errors is complicated - I could spawn_link but if there’s any error in the task then the view reloads, which is ok sometimes but not if the error is due to, say, an upstream service being down, which would lead to an infinite loop of reloading. I can monitor the process, it just feels clunky to need to do that on my own.

I’m curious if anyone has come across a better pattern for this, or maybe there’s something obvious I have just missed.

Most Liked Responses

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

I think the thing here is less about managing access to the remote server and more about how to not block your liveview processes whether you’re directly pinging the remote endpoint or whether it goes through a genserver.

@dantswain this is a good question! We’ve done basically the same thing (I think we use a task but same idea) and it definitely is a challenge for testing. I think our solution involved setting an assign which controlled whether it was done sync or async, and then in the tests we set that assign to sync. Definitely an area that could use some slightly more ergonomic patterns though!

dantswain

dantswain

Thanks for the good suggestion @vsoraki. We generally do use a similar pattern to that to mock out our api calls. The thing that makes this difficult to test is that the call (mocked or not) is being done in a process that is spawned from the view. So to test it, we need to know when that separate process has finished, sent a message back to the view process, and the view process has consumed that message.

The simplest way to do that is to repeatedly render the view and check for the effect that you expect, but that can be slow and fail in unexpected ways. What we’ve been doing is actually using the API mock (using Tesla) to capture the pid of the spawned process and relay it back to the test process, then have the test process monitor the spawned process and assert_receive that we get the :DOWN message for the spawned pid. This works ok most of the time but is still sometimes flakey (false negatives) and it requires a bit of overhead in terms of set up.

It looks something like this

test "fetches data from API and renders results", %{conn: conn} do
  test_pid  = self()   
  tag = :api_call # tag because we may have multiple APIs to call

  Tesla.Mock.mock(fn ->
    send(test_pid, {tag, self()})
    {:ok, "Hello from the API"}
  end)

  {:ok, view, _initial_html} = live(conn, "/")

  assert_receive {:api_call, spawned_pid}
  ref = Process.monitor(spawned_pid)
  assert_receive {:DOWN, ^ref, :process, _pid, _reason}, 1_000

  assert render(view) =~ "Hello from the API"
end
Supamic

Supamic

I just wanted to post this shorter ElixirConf talk and accompanying github repo this for anyone else landing here looking for more recent information on this topic because I found it very useful.

https://github.com/chrisgreg/async_tasks

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement