Ash.load with timeout option seems to be ignored with custom read_action

In my resource, I have this relationship:

    has_one :similar_grantee_entity, Entity do
      no_attributes? true

      read_action :read_in_temp

      filter expr(...)
    end

In my code, I load it using:

Ash.load!(records, :similar_grantee_entity, timeout: :infinity)

As you can see, I’m setting the timeout option, but even with that, I’m getting the following error:

** (Ash.Error.Invalid)
Bread Crumbs:
  > Error returned from: Core.Pacman.Markets.Entity.read_in_temp
  > Error returned from: Core.Pacman.Markets.Record.read

Invalid Error

* Core.Pacman.Markets.Entity.read_in_temp timed out after 180000ms.

The default timeout can be configured on the domain,

    execution do
      timeout :timer.seconds(60)
    end

Each request can be configured with a timeout via `Ash.Changeset.timeout/2` or `Ash.Query.timeout/2`.

    at similar_grantee_entity
  (ash 3.5.6) lib/ash/error/invalid/timeout.ex:5: Ash.Error.Invalid.Timeout.exception/1
  (ash 3.5.6) lib/ash/process_helpers.ex:86: Ash.ProcessHelpers.task_with_timeout/5
  (ash 3.5.6) lib/ash/actions/read/read.ex:970: Ash.Actions.Read.maybe_in_transaction/3
  (ash 3.5.6) lib/ash/actions/read/read.ex:326: Ash.Actions.Read.do_run/3
  (ash 3.5.6) lib/ash/actions/read/read.ex:89: anonymous fn/3 in Ash.Actions.Read.run/3
  (ash 3.5.6) lib/ash/actions/read/read.ex:88: Ash.Actions.Read.run/3
  (ash 3.5.6) lib/ash/actions/read/relationships.ex:542: anonymous fn/3 in Ash.Actions.Read.Relationships.do_fetch_related_records/5
    (ash 3.5.6) lib/ash/error/invalid.ex:3: Ash.Error.Invalid.exception/1
    (ash 3.5.6) /home/runner/work/platform/platform/core/deps/splode/lib/splode.ex:264: Ash.Error.to_class/2
    (ash 3.5.6) lib/ash/error/error.ex:108: Ash.Error.to_error_class/2
    (ash 3.5.6) lib/ash/actions/read/read.ex:414: anonymous fn/3 in Ash.Actions.Read.do_run/3
    (ash 3.5.6) lib/ash/actions/read/read.ex:342: Ash.Actions.Read.do_run/3
    (ash 3.5.6) lib/ash/actions/read/read.ex:89: anonymous fn/3 in Ash.Actions.Read.run/3
    iex:10: (file)

Is this a bug?

:thinking: interesting. I think it may be, but it’s a bit complicated. The load itself does a read which has a timeout on it, and I don’t think we do anything to indicate to load read actions that they should adopt the parent’s timeout, and its not entirely clear if they should adopt the parent’s timeout TBH. Its…unclear :slight_smile:

You could conditionally add the timeout in the target action for only when its being run directly by switching on the presence of query.context[:accessing_from] I believe.

But if I don’t specify a read_action then the load would respect the timeout value correct?

At least that is what I would expect from a timeout field in a load call haha. And, of course, if that is the case, I would expect the same to be true if the read_action is set as that is just a implementation detail that the caller (the one doing the load) doesn’t need to concern about it.

I actually think that this particular behavior is more correct, considering multiple options. The concept is that when loading relationships we honor the rules on the relevant read action. That action is, by default, the primary read action. You’ve set a timeout on that read action, which means it applies when being used to load relationships also.

By “this particular behavior” you mean applying the load timeout value to all read calls the function does behind the scene or the other way around?

You’ve set a timeout on that read action, which means it applies when being used to load relationships also.

Where was that set actually? What I set was only a custom read action with the read_action option and then I set the load timeout in the load call which I expected to be applied to that custom read call too.

Think about it like this:

with_timeout(:infinity, fn -> 
  # this timeout is where the error comes from
  with_timeout(180000, fn -> 
     :timer.sleep(500000)
  end)
end)

The related action used for the read has a timeout. Just because the outer action call has an infinite timeout, it still calls something with its own shorter timeout.

Ah, I see, but in that case, shouldn’t the inner call inherit the timeout of the outer call? Otherwise, what exactly is the point of the timeout option in a load call?

Actually, what exactly does the timeout in the load function do? Will it not set the relationship load query timeout too?

The timeout passed to the load is “how long to wait for all loads”, not a value to use for each individual load.

In that case, how can someone set/customize the individual loads timeout value?

You’d need to do one of:

  • use a different action
  • In the action, set the timeout conditionally depending on something like context, and load with that, I.e load(foo: Ash.Query.set_timeout(...))
  • Use relationship context and switch on that in the destination action conditionally, i.e
has_many ... do
  context %{...}
end

If you don’t mind, can you give me a more concrete example of these uses or link me to where they are described in the documentation? I’m not sure I understand exactly how to apply them.

use a different action

Why would using a different action helps here? :read_in_temp is already a custom action that is only used by this relationship.

In the action, set the timeout conditionally depending on something like context, and load with that, I.e load(foo: Ash.Query.set_timeout(...))

This would only works if the action itself is doing the load right? In my case I’m doing the load manually using Ash.load directly. Also, what if I need to load other inner relationships (load(foo: :bar)), in that case I would not be able to add the Ash.Query call right?

Use relationship context and switch on that in the destination action conditionally, i.e

What you mean here is add some field to the context like%{running_as_load?: true} and then add a prepare function that would catch that and set another timeout correct?
But this would not allow me to set the timeout value from “outside” the resource in the Ash.load call correct?

Interesting, I found this test that uses load with a query function:

https://github.com/ash-project/ash/blob/d78909faedd0fd56ac46d8f6286b50bda4bc1884/test/query/default_sort_in_relationship.exs#L155

But when I try to do the same:

record |> Ash.load(similar_grantee_entity: fn query -> query end)

I get:

** (FunctionClauseError) no function clause matching in Ash.Resource.Info.attribute/2

    The following arguments were given to Ash.Resource.Info.attribute/2:

        # 1
        Core.Pacman.Markets.Entity

        # 2
        #Function<42.18682967/1 in :erl_eval.expr/6>

    Attempted function clauses (showing 1 out of 1):

        def attribute(resource, name) when is_binary(name) or is_atom(name)

    (ash 3.5.6) lib/ash/resource/info.ex:743: Ash.Resource.Info.attribute/2
    (ash 3.5.6) lib/ash/query/query.ex:1737: Ash.Query.do_load/2
    (elixir 1.18.1) lib/enum.ex:2546: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ash 3.5.6) lib/ash/query/query.ex:1544: anonymous fn/3 in Ash.Query.load/3
    (elixir 1.18.1) lib/enum.ex:2546: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ash 3.5.6) lib/ash.ex:1900: Ash.load/3
    (ash 3.5.6) lib/ash.ex:1864: Ash.load/3
    iex:20: (file)

Which would make sense if I just look at the Ash.load typespec, but, at the same time, that makes me wonder how that test is working at all

Right you are, that test is not properly suffixed with _test.exs, so its not being run.

Lets start over here.

Is there a timeout set on the read_in_temp action?

Nop, it just changes the schema:

    read :read_in_temp do
      prepare fn query, _ ->
        Ash.Query.set_context(query, %{data_layer: %{schema: "temp_entities"}})
      end
    end

Hey @zachdaniel just a quick bump since this still creates severe issues to our system :sweat_smile:

The only way that I think I can fix it now, if I understood correctly, is to create a custom read action where I set the timeout field of it in the action itself.

I guess that would work, but it seems like a very inflexible solution.

For example, let’s say I know that the read action will return a bunch of results instead of just a few, in this case, I need to have a very big timeout set to that action just because, sometimes, I need to fetch more data.

Same issue if I know that the db is under heavy load, since I can’t customize the timeout of the relationships when calling the action, my only solution is assume that the DB is always under load and set a default timeout that is pretty big which can be undesirable in some cases.

You did mention this:

But I still can’t understand how exactly I should use this as this doesn’t seem to be a valid call since set_timeout doesn’t exists and timeout requires a query as first argument.

You can build a query that represents that load. And a load statement can be a query.

Sure, that’s an alternative, but then, I’m moving complexity outside the resource, which I would prefer not to if there is another way

I think I need to see a more comprehensive example of the actions and relationships at play. Where is the 180_000ms timeout coming from? How is it being set. I don’t think I’ve seen everything at play here yet?

The 180_000 is comming from the default timeout defined in the domain:

  execution do
    timeout :timer.minutes(3)
  end