Skip to content

Interactions with Ecto Sandbox #150

@benwilson512

Description

@benwilson512

Hey folks!

Thanks for making this very cool library. We are trying to retrofit Carbonite into an existing code base and are running into some potentially problematic interactions with the Ecto Sandbox. Consider this test:

test "inserts after an audit should raise" do
  MyApp.Repo.transaction(fn ->
    {:ok, _} = Carbonite.insert_transaction(MyApp.Repo, %{meta: %{type: "rabbit_inserted"}})

    %{}
    |> MyApp.Rabbit.create_changeset()
    |> MyApp.Repo.insert!
  end)
    
  # This isn't inside a carbonite transaction and should error
  %{}
  |> MyApp.Rabbit.create_changeset()
  |> MyApp.Repo.insert!

end

The second Repo.insert! should fail and it WOULD fail if you ran this in your dev console or in production. However in an ecto sandbox test, this will NOT fail. And the reason of course is that the entire test is run inside of a transaction that has already been created by Ecto, and so when the trigger runs on the second insert it is able to find the carbonite record we inserted earlier.

The consequence of this in particular when retrofitting Carbonite into an existing code base is that the tests are not able to provide ANY confidence that we have adequate coverage around transaction insertion. As long as there is any transaction inserted in the test setup, every subsequent insert / update will work just fine. When you run that same code in production, errors will abound.

Our "Solution"

The workaround we have is that in tests, we do:

  def attach_commit_handler() do
    :telemetry.attach(
      "audit_commit_sim",
      [:sensetra, :repo, :query],
      fn _, _, metadata, _ ->
        if String.upcase(metadata.query) == "COMMIT" do
          import Ecto.Query

          Carbonite.Transaction
          |> update([q], set: [xact_id: fragment("0::text::xid8")])
          |> Sensetra.Repo.update_all([])

          Carbonite.Change
          |> update([q], set: [transaction_xact_id: fragment("0::text::xid8")])
          |> Sensetra.Repo.update_all([])
        end
      end,
      %{}
    )
  end

In the ecto sandbox, there is still a COMMIT issued it just doesn't commit the outer sandbox transaction due to how the sandbox uses save points. Thus we can use the ecto telemetry to hook into this COMMIT and alter the saved xact_id values on any inserted carbonite transactions and changes. This way when a follow up insert happens AFTER a transaction is over, it can't / won't find any matching transaction record, and fails as intended.

While this works, it does feel like a bit of a strange pain point. What are other people doing for this situation?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions