How to correctly return error of read action with transaction?

In my resource, I have a read action that needs to set pg_trgm.word_similarity_threshold before doing the filter query. To do that I created this preparation:

defmodule Core.Ash.Preparations.WordSimilarityThreshold do
  @moduledoc false

  use Ash.Resource.Preparation

  def prepare(query, opts, _context) do
    Ash.Query.before_action(query, fn query ->
      repo = Keyword.get(opts, :repo, Core.Repo)

      similarity_threshold = Map.get(query.arguments, :similarity_threshold, 0.80)

      repo.query!("set pg_trgm.word_similarity_threshold = #{similarity_threshold}", [])

      query
    end)
  end
end

And then i can use it like this

      prepare WordSimilarityThreshold

      filter expr(fragment("(? %> ?)", full_address_normalized, ^arg(:address)))

This works fine, but if the query fails for some reason (for example, a timeout), instead of getting the timeout error, I will get an rollback unknown error because the query is inside a transaction.

** (Ash.Error.Unknown)
Bread Crumbs:
  > Error returned from: Core.Pacman.Markets.Property.list_by_address

Unknown Error

* unknown error: :rollback
  (ash 3.4.60) /var/home/sezdocs/projects/rebuilt/platform/core/deps/splode/lib/splode.ex:344: Ash.Error.to_error/2
  (elixir 1.18.1) lib/enum.ex:1714: Enum."-map/2-lists^map/1-1-"/2
  (ash 3.4.60) /var/home/sezdocs/projects/rebuilt/platform/core/deps/splode/lib/splode.ex:229: Ash.Error.to_class/2
  (ash 3.4.60) lib/ash/error/error.ex:108: Ash.Error.to_error_class/2
  (ash 3.4.60) lib/ash/actions/read/read.ex:395: anonymous fn/3 in Ash.Actions.Read.do_run/3
  (ash 3.4.60) lib/ash/actions/read/read.ex:324: Ash.Actions.Read.do_run/3
  (ash 3.4.60) lib/ash/actions/read/read.ex:82: anonymous fn/3 in Ash.Actions.Read.run/3
  (ash 3.4.60) lib/ash/actions/read/read.ex:81: Ash.Actions.Read.run/3
  (ash 3.4.60) lib/ash.ex:2014: Ash.read/2
  (ash 3.4.60) lib/ash.ex:1972: Ash.read!/2
  (core 1.175.0) lib/core_web/components/public_record.ex:488: anonymous fn/2 in CoreWeb.Components.PublicRecord.find_property/2
  (phoenix_live_view 1.0.9) lib/phoenix_live_view/async.ex:213: Phoenix.LiveView.Async.do_async/5
  (elixir 1.18.1) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2
    (ash 3.4.60) lib/ash/error/unknown.ex:3: Ash.Error.Unknown."exception (overridable 2)"/1
    (ash 3.4.60) /var/home/sezdocs/projects/rebuilt/platform/core/deps/splode/lib/splode.ex:264: Ash.Error.to_class/2
    (ash 3.4.60) lib/ash/error/error.ex:108: Ash.Error.to_error_class/2
    (ash 3.4.60) lib/ash/actions/read/read.ex:395: anonymous fn/3 in Ash.Actions.Read.do_run/3
    (ash 3.4.60) lib/ash/actions/read/read.ex:324: Ash.Actions.Read.do_run/3
    (ash 3.4.60) lib/ash/actions/read/read.ex:82: anonymous fn/3 in Ash.Actions.Read.run/3
    (ash 3.4.60) lib/ash/actions/read/read.ex:81: Ash.Actions.Read.run/3
    (ash 3.4.60) lib/ash.ex:2014: Ash.read/2
    (ash 3.4.60) lib/ash.ex:1972: Ash.read!/2
    (core 1.175.0) lib/core_web/components/public_record.ex:488: anonymous fn/2 in CoreWeb.Components.PublicRecord.find_property/2
    (phoenix_live_view 1.0.9) lib/phoenix_live_view/async.ex:213: Phoenix.LiveView.Async.do_async/5
    (elixir 1.18.1) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2
Function: #Function<7.1988948/0 in Phoenix.LiveView.Async.run_async_task/5>
    Args: []

Is this an Ash bug or is this the expected behavior? Is there some way to make it return the timeout error instead?

This looks like a bug to me. Somewhere in :read logic not handling that rolling back properly. Could you create a reproduction and I’ll check it out? You should never really see that :rollback atom in errors.

Here is a resource that will trigger the bug:

defmodule Resource do
  use Ash.Resource, domain: Domain, data_layer: AshPostgres.DataLayer

  attributes do
    uuid_v7_primary_key :id

    attribute :full_address_normalized, :string, allow_nil?: false
  end

  postgres do
    table "properties"
    repo Core.Repo
  end

  actions do
    defmodule WordSimilarityThreshold do
      use Ash.Resource.Preparation

      def prepare(query, opts, _context) do
        Ash.Query.before_action(query, fn query ->
          similarity_threshold = Map.get(query.arguments, :similarity_threshold, 0.80)

          Core.Repo.query!("set pg_trgm.word_similarity_threshold = #{similarity_threshold}", [])

          query
        end)
      end
    end

    read :list do
      transaction? true

      argument :address, :string, allow_nil?: false

      prepare WordSimilarityThreshold

      prepare fn query, _ ->
        Ash.Query.after_action(query, fn _query, results ->
          Process.sleep(5_000)

          results
        end)
      end

      filter expr(fragment("(? %> ?)", full_address_normalized, ^arg(:address)))
    end
  end
end

Resource |> Ash.Query.for_read(:list, %{address: "blibs"}) |> Ash.read(timeout: 3_000)

Please if possible create a full project w/ migrations and everything set up, because I’m going to have to set up a project w/ pg_trgm and generate the migrations and set up the domains etc. It takes a lot longer on my end to back into an app I can test with, and I usually end up spending far more time on just making sure I’m reproducing the issue properly and repeatedly than actually fixing the issue :cry:

No problem! There it is: GitHub - sezaru/test_read_transaction

To trigger it, just run TestReadTransaction.trigger

One more thing: would you mind opening a GH issue to track it? And include the repro and details there? I will try to get to it early next week :slight_smile:

Sorry, it took me some time to go back to this, here is the issue: read action returns rollback error inside transactions · Issue #1997 · ash-project/ash · GitHub