TurtleAI / derive

An Event Sourcing and CQRS solution.
0 stars 1 forks source link

Detect key of operation when `Ecto.Multi` fails on an `Ecto.QueryError` #5

Closed venkatd closed 2 years ago

venkatd commented 2 years ago

Hi @TurtleAI/eds

I'm finding that when Ecto.Repo.transaction fails due to something like a constraint error, I'm unable to find the key of the operation that failed.

I'd like to show a more helpful message that correlates an event to the Ecto operation that triggered it and getting the key would help here.

I have a try ... rescue here: https://github.com/TurtleAI/derive/blob/b430a9a318efe66fc62076ea5ceded7e5f4a9f12/lib/derive/state/ecto.ex#L39

And I rescue an error of this form:

%Ecto.QueryError{message: "deps/ecto/lib/ecto/repo/queryable.ex:166: field `missing_field` in `update` does not exist in schema DeriveEctoTest.User in query:\n\nfrom u0 in DeriveEctoTest.User,\n  where: u0.id == ^\"99\",\n  update: [set: [missing_field: ^\"stuff\"]]\n"}

Is there a way to get the name of Ecto.Multi operation that this error was triggered for? Or if this isn't possible, maybe there's a way to validate these individual operations before executing them?

Thanks!

wojtekmach commented 2 years ago

In general, we'd know the operation name if we gracefully handle errors in our multi. In such case, the return from Repo.transaction would be: {:error, operation_name, changeset, operations}. For uniqueness constraint specifically this would only work when we gracefully handle it with Ecto.Changeset.unique_constraint/3. If don't gracefully handle errors, if we crash out of our transaction, unfortunately there is no good way to connect the crash to the operation that caused it. Well, there's always the process dictionary escape hatch, we could do this:

    multi =
      Ecto.Multi.new()
      |> Ecto.Multi.run(
        :foo,
        fn _repo, _changes ->
          Process.put(:multi_operation_name, :foo)
          raise "oops"
        end
      )

    try do
      Repo.transaction(multi)
    rescue
      e ->
        IO.inspect(multi_operation_name: Process.get(:multi_operation_name))
        reraise e, __STACKTRACE__
    end

But yeah, this is definitely a last resort sort of thing and as anytime we use pdict, it's asking for trouble down the road.

venkatd commented 2 years ago

Hi @wojtekmach thanks!

I guess as a compromise we can at least return the entire Ecto.Multi. I want to make it easier to identify mistakes during development. So if I write a bad query/multi in a particular handler, I can point to the exact line of code+event where the mistake happened.

I'll avoid the process solution if I can, but good to know that this is an option!