Why there is no `Ash.Changeset.around_transaction`?

Eduardo B. Alexandre
2023-06-25

Eduardo B. Alexandre:

I was wondering why there is no around_transaction function in Ash.Changeset . We already have a around_action option, but that one runs inside a transaction, meaning that I can’t use it if I wan’t to add something to the DB regardless if the action itself fails or not.

I know that there is a before_transaction and after_transaction , but depending on what I’m doing this would not work.

Just to give a more concrete example.

I’m planning to port this into a Ash change:

  def lock_and_transact(user, type, apply_function, update_function) do
    Mutex.under(SubscribeMutex, user.uuid, fn ->
      response =
        case create_transaction(user, type) do
          {:ok, _} ->
            {:ok, response} = apply_function.()
            
            response

          {:error, _} ->
            user = update_function(user)

            {:error, :unfinished_transaction, handle_error!(type, user)}
        end

      delete_transaction!(user, type)

      response
    end)
  end

The idea in this function is that I will first use Mutex to make sure that I always have only one code path reaching this code block at a time, after it, I create a transaction, which basically means that I have a transaction table and I store a row there for that user and type .

I then run the apply_function , if that function doesn’t return an error, it will call the delete_transaction! function that will remove the transaction and return the response, otherwise, it will just crash leaving the added transaction in the DB, this means that next time this code is called, it will fail to create a transaction and it will run the update_function instead.

If I just wanted to create the transaction row in the DB, I would be able to create two changes, one with after_transaction (to create the row) and one with before_transaction (to delete it), but that doesn’t work with the Mutex call, for the mutex I need something like around_action but for transactions.

zachdaniel:

We could potentially add an around_transaction hook, it just hadn’t come up before

zachdaniel:

In the meantime, you can make a manual action, set transaction? false and then in the manual action implementation call the appropriate action with changeset.params

Eduardo B. Alexandre:

I can do that, but then I would need to create one manual action for each action that I want to use this right?

zachdaniel:

Yes, unfortunately

Eduardo B. Alexandre:

Would you say this is something in the roadmap? I was looking into the code to see if I could do a PR, but seems like it is not somthing that I can do without spending some time understanding the Ash.Changeset code first

zachdaniel:

Something to keep in mind with transaction hooks is that they don’t work when composed with other resource actions. It only works if the action is the “top level” action. The hooks will still fire, but if they are already in a transaction then they won’t of course be “around” the transaction.

zachdaniel:

Its probably not that hard to accomplish tbh

zachdaniel:

There is a function called with_hooks in changeset that you’d basically copy the implementation of the around action hooks, and call that first thing in that with hooks function

Eduardo B. Alexandre:

Something to keep in mind with transaction hooks is that they don’t work when composed with other resource actions. It only works if the action is the “top level” action. The hooks will still fire, but if they are already in a transaction then they won’t of course be “around” the transaction. That’s fine, the idea is to only use it in “top level” actions anyway 🙂

Eduardo B. Alexandre:

<@197905764424089601> can you take a look into this PR? https://github.com/ash-project/ash/pull/632

Eduardo B. Alexandre:

Seems to work for me, but I’m not sure if I missed some corner case

zachdaniel:

That looks right to me. Will review more thoroughly when I get home. Well want to warn on any around transaction hooks like we do the other ones

zachdaniel:

Like add this to the top of the function before running the around transaction hooks:

warn_on_transaction_hooks(changeset, changeset.around_transaction, “around_transaction”)

Eduardo B. Alexandre:

I pushed a commit with that change