Absinthe resolving: is it possible to compose resolving batching & async together?

Hi all, currently i am in the middle of optimizing a resolution of a particular field in graphql using absinthe, which is particularly interesting because it has elements that should be batched, and element that should be asynced. I have a working implementation, but it’s somewhat hacky and i wonder if it’s possible to do this another way? (maybe there’s a way to compose batching & async together)

the original code looks somewhat like this

# Absinthe Type
object :order do
  #....
  field :server_company, :company do
    resolve(fn order, _input, _ ->
        # here's SQL call, it should be batched for optimization
        order_request = Repo.preload(order, :order_request).order_request

        server_company_id =
          if is_nil(order.server_company_id) and
               order_request.request_type == "PREORDER" do
            # here's also SQL call, it should be batched for optimization
            order_request = Repo.preload(order_request, :preorder_data)

            case order_request.preorder_data do
              %{server_company_id: server_company_id} ->
                server_company_id

              _ ->
                nil
            end
          else
            order.server_company_id
          end

        # Here's API call to other service, should be asynced for optimization 
        {:ok, Servers.get_company_data(server_company_id)}
      end)
  end
end

above code have part which should be optimized by batching, and part which should be optimized by async. currently the hacky implementation is based on moving async calling inside the batching code, it looks like:

# Absinthe Type
object :order do
  #....
  field :server_company, :company do
    resolve(fn order, _input, _ ->
        batch({__MODULE__, :batch_order_server_company}, order, fn batch_results ->
          {:ok, Map.get(batch_results, order.id)}
        end)
    end)
  end
end

def batch_order_server_company(_, orders) do
  orders = Repo.preload(orders, [order_request: :preorder_data])
  
  tasks = for order <- orders do
    order_request = order.order_request
    
    server_company_id =
      if is_nil(order.server_company_id) and
        order_request.request_type == "PREORDER" do
        case order_request.preorder_data do
          %{server_company_id: server_company_id} ->
            server_company_id
          _ ->
            nil
          end
      else
        order.server_company_id
      end
   
   Task.async(fn -> {order.id, Servers.get_company_data(server_company_id)} end)
  end
  Task.await_many(tasks) |> Enum.into(%{})
end

I was using somewhat customized async resolver, which have separate supervisor for async task and implement latency cutting by backup requests, so if possible i like to reuse functionality inside the async resolver (though if i need i could still refactor important logic part from async resolver if needed).

Now that i think about it, this also not particularly correct optimization in respect of absinthe async behaviour. if i understand it correctly, when absinthe do async, it would just fire up a new task, then do other thing (resolve other field, etc.) then only after sometime it would then wait on the async task. This piece of code however await the task directly.

After reading documentation on absinthe carefully, specifically at Absinthe.Middleware.Batch — absinthe v1.7.6 , there stated that

  • fn batch_results: This function takes the results from the batching function. it should return one of the resolution function values.

after some more reading, turn out that resolution function values means result type on Absinthe.Type.Field — absinthe v1.7.6 which are able to receive middleware triplet tuple as accepted type. so in principle, it’s possible to do code like:

# Absinthe Type
object :order do
  #....
  field :server_company, :company do
    resolve(fn order, _input, _ ->
        batch({__MODULE__, :batch_order_server_company_id}, order, fn batch_results ->
          server_company_id = Map.get(batch_results, order.id)
          async(fn -> {:ok, Servers.get_company_data(server_company_id)} end)
        end)
    end)
  end
end

though in the end i moved away to using batch for all the optimization, so i haven’t really tested code above.

Yup yeah you can compose them this way. In general though because different batches are themselves run in parallel it’s almost always better to just use batching rather than async.

1 Like