Catharz

Catharz

Graceful fallbacks using Absinthe async helper

We’ve been using Absinthe to power a mobile BFF in a kubernetes environment for several years now. One of the challenging aspects of this has been dealing with failures in a graceful way. Especially, in an environment where the apps we have dependencies on (including our rails monolith) are deployed dozens of times every day. The other challenging aspect has been ensuring that we can fetch as much data asynchronously as possible.

We originally used Absinthe’s async middleware, but soon discovered that it didn’t fail in a “nice” enough way (for our liking) when timeouts occurred. So we started creating things like this:

  def get_cart(args, %{context: %{forwarded_headers: headers}}) do
    span_context = %Spandex.SpanContext{
      trace_id: Tracer.current_trace_id(),
      parent_id: Tracer.current_span_id()
    }

    [&fetch_cart/3, &fetch_coupon/3, &fetch_messages/3]
    |> Task.async_stream(& &1.(args, headers, span_context),
      ordered: true,
      on_timeout: :kill_task,
      timeout: @cart_timeout,
      max_concurrency: 3
    )
    |> Enum.into([], fn {status, res} ->
      case status do
        :ok ->
          res

        _ ->
          nil
      end
    end)
    |> merge_responses()
  end

When I look at this, I think: “there must be a better way”.

I’m working on a PR to change the Absinthe async helper to use Task.async_stream/3 instead of Task.await and provide a fallback option. But I’m not sure how to write a “good” test for the timeout aspect, and I’m not sure there isn’t a much better option than what I’m proposing.

Is there a better way?

Most Liked

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

So the fundamental issue here isn’t so much that Task.async_stream is horrible for this case since you do in fact have 3 things to do, but mostly I just want to point out that the way this works is fundamentally different than async middleware / resolvers.

What you have here is not an async resolver. This is a synchronous resolver that, internally, spawns and then awaits upon 3 tasks in a blocking way. No other part of your GraphQL query is being processed while this resolver runs. By contrast, asynchronous resolvers start a task and then suspend the middleware, allowing Absinthe to move forward and initiate the resolution of other fields.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

@Catharz there is a very easy reason why Task.async_stream isn’t used here: I don’t think it was invented yet when I wrote this code

If we were to refactor this code to use async_stream, we need to be careful not to change the semantics of how the telemetry events relate to each task. Right now the start events are emitted just prior to each task being spawned. The stop events are actually coded in a way that I think is bad, in that emitting the stop events for tasks that finish early will be delayed if they are in a later group behind tasks that finish later.

I do believe we could get the same start semantics using Stream.map so that the telemetry events were evaluated before each item. The ending semantics could be improved by doing ordered: false to the async_stream and doing a Stream.map afterward. PR welcome! :smiley:

Catharz

Catharz

One of the complexities I found with running an Elixir app in K8s is Task.async_stream/5 uses System.schedulers_online/0 to determine how many operations to run asynchronously. This returns the number of cores assigned to the pod (which in our environment is 1), which means nothing runs async when using the default options.

That’s why we explicitly set max_concurrency to the number of tasks above. To handle this, I’m going to set max_concurrency to the number of fields we want to resolve asynchronously. This means this will work quite differently to the current async helper, so I’m going to create a new AsyncStream helper instead of changing the existing Async helper.

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
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
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement