ASH Framework race condition when side loading data

I am working through Stefan Wintermeyer’s “Relationships” tutorial and I have a race condition occurring. There is no error but the function call does not return and all the processors on my laptop peg out.

I am in the section “many_to_many sideloading by default” and have made the edits to product.ex and tag.ex.

(Sidenote the documentation has a mistake in the file name reference; it has “lib/app/product.ex” listed twice and in the second reference it should read “lib/app/tag.ex”.)

    # lib/shop/tag.ex
    defaults [:update, :destroy] 

    read :read do
      primary? true
      prepare build(load: [:products]) 
    end
    # lib/shop/product.ex
    defaults [:update, :destroy]

    read :read do
      primary? true
      prepare build(load: [:tags])
    end

Then if you add data via iex:

alias App.Shop.Tag
alias App.Shop.Product
sweet = Tag.create!(%{name: "Sweet"})
tropical = Tag.create!(%{name: "Tropical"})
red = Tag.create!(%{name: "Red"})
Product.create!(%{name: "Apple", tags: [sweet, red]})
Product.create!(%{name: "Banana", tags: [sweet, tropical]})
Product.create!(%{name: "Cherry", tags: [red]})

Then call either Product.read!() or Tag.read!() the race condition occurs.

If you comment out the new edits and return the code to the original state before the edits in just one of the files, the race condition goes away. The file with the edits remaining works as advertised.

edited: my code snippet was duplicated corrected to what is actually in the file.

@wintermeyer to let you know.

If two resources load each other like that they will go back and forth forever loading each other. There isn’t really a way to address it in the core, but what you can do is set context on the loading query to tell it not to load the thing it’s going to load. Both require custom preparations (which are not complicated to write). At the end of the day, this is one of the many reasons why I suggest not loading data by default, and always having the caller specify what they need and/or making arguments on an action that make it load things, i.e

read :read do
  argument :load_tags?, :boolean, allow_nil?: false, default: false

  prepare fn query, _ -> 
    if query.arguments[:load_tags?] do
      Ash.Query.load(query, :tags)
    else
      query
    end
  end
end
1 Like

Thank you for the quick response! Good to know! I’ll heed your advice and not do any more default sideloading.