Error handling in Ecto.Multi update_all operation?

I have a working Ecto.Multi transaction, with several bank-transaction-like constraints, that several vales can’t be negative… this works, but when the constraints are violated, I’m using update_all not update, so using queries not changesets, and my changesets know about these constraints, but I don’t see how to add an on_conflict or other constraint awareness to an Ecto.Multi query based transaction…

I could rescue the error, but that doesn’t seem right…code is on this commit and like I said, it works on happy path, but when I intentionally do a big illegal transaction I’d expect a way to get an {:error, reason} tuple or something out, and instead I get a big fat error, and it looks like update_all doesn’t accept on_conflict /conflict_target options like I might use otherwise (would be OK with an on_conflict: :nothing and getting an error from transaction for now, FWIW)

3 Likes

I had a chance to ask Chris McCord about this after his talk at NYC last week, and his advise was to move each update_all operation into a function and cal with Ecto.Multi.run, and put the rescue clause inside the run function, so the function returns :ok/:error tuple with error coming from a rescue of the database error. For now I just threw the whole transaction into a spawn block so that a crash doesn’t take down the channel doing the transaction, and since I effectively want on_conflict: :nothing for now, this achieves it… if I want to notify client of the errror later (which I may, as it is the client will have optimistically assumed success, but in my testing so far it’s pretty self-correcting because of the data I send back on other events) I plan to refactor to Chris’s suggested Multi.run plan, or maybe even as an experiment sooner if I have time. Just thought I’d capture here in case anyone else comes across the issue and finds this looking for advice! (and/or for future me if I forget!)

4 Likes

I faced a similar situation and implemented the following. Sort of what like Chris suggested. update_transactions_status/3 is called from Ecto.Multi.run.

  defp update_transactions_status(_multi, ids, status) do
    target_count = Enum.count(ids)
    {count, nil} =
      Repo.update_all (from t in Transaction, where: t.id in ^ids),
                      [set: [status: status, updated_at: NaiveDateTime.utc_now()]]
    if target_count == count do
      {:ok, count}
    else
      {:error, count}
    end
4 Likes