697 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			697 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
defmodule Ecto.Adapters.SQL.Sandbox do
 | 
						|
  @moduledoc ~S"""
 | 
						|
  A pool for concurrent transactional tests.
 | 
						|
 | 
						|
  The sandbox pool is implemented on top of an ownership mechanism.
 | 
						|
  When started, the pool is in automatic mode, which means the
 | 
						|
  repository will automatically check connections out as with any
 | 
						|
  other pool.
 | 
						|
 | 
						|
  The `mode/2` function can be used to change the pool mode from
 | 
						|
  automatic to either manual or shared. In the latter two modes,
 | 
						|
  the connection must be explicitly checked out before use.
 | 
						|
  When explicit checkouts are made, the sandbox will wrap the
 | 
						|
  connection in a transaction by default and control who has
 | 
						|
  access to it. This means developers have a safe mechanism for
 | 
						|
  running concurrent tests against the database.
 | 
						|
 | 
						|
  ## Database support
 | 
						|
 | 
						|
  While both PostgreSQL and MySQL support SQL Sandbox, only PostgreSQL
 | 
						|
  supports concurrent tests while running the SQL Sandbox. Therefore, do
 | 
						|
  not run concurrent tests with MySQL as you may run into deadlocks due to
 | 
						|
  its transaction implementation.
 | 
						|
 | 
						|
  ## Example
 | 
						|
 | 
						|
  The first step is to configure your database to use the
 | 
						|
  `Ecto.Adapters.SQL.Sandbox` pool. You set those options in your
 | 
						|
  `config/config.exs` (or preferably `config/test.exs`) if you
 | 
						|
  haven't yet:
 | 
						|
 | 
						|
      config :my_app, Repo,
 | 
						|
        pool: Ecto.Adapters.SQL.Sandbox
 | 
						|
 | 
						|
  Now with the test database properly configured, you can write
 | 
						|
  transactional tests:
 | 
						|
 | 
						|
      # At the end of your test_helper.exs
 | 
						|
      # Set the pool mode to manual for explicit checkouts
 | 
						|
      Ecto.Adapters.SQL.Sandbox.mode(Repo, :manual)
 | 
						|
 | 
						|
      defmodule PostTest do
 | 
						|
        # Once the mode is manual, tests can also be async
 | 
						|
        use ExUnit.Case, async: true
 | 
						|
 | 
						|
        setup do
 | 
						|
          # Explicitly get a connection before each test
 | 
						|
          :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
 | 
						|
        end
 | 
						|
 | 
						|
        setup tags do
 | 
						|
          pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo, shared: not tags[:async])
 | 
						|
          on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
 | 
						|
          :ok
 | 
						|
        end
 | 
						|
 | 
						|
        test "create post" do
 | 
						|
          # Use the repository as usual
 | 
						|
          assert %Post{} = Repo.insert!(%Post{})
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
  ## Collaborating processes
 | 
						|
 | 
						|
  The example above is straight-forward because we have only
 | 
						|
  a single process using the database connection. However,
 | 
						|
  sometimes a test may need to interact with multiple processes,
 | 
						|
  all using the same connection so they all belong to the same
 | 
						|
  transaction.
 | 
						|
 | 
						|
  Before we discuss solutions, let's see what happens if we try
 | 
						|
  to use a connection from a new process without explicitly
 | 
						|
  checking it out first:
 | 
						|
 | 
						|
      setup do
 | 
						|
        # Explicitly get a connection before each test
 | 
						|
        :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
 | 
						|
      end
 | 
						|
 | 
						|
      test "calls worker that runs a query" do
 | 
						|
        GenServer.call(MyApp.Worker, :run_query)
 | 
						|
      end
 | 
						|
 | 
						|
  The test above will fail with an error similar to:
 | 
						|
 | 
						|
      ** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.35.0>
 | 
						|
 | 
						|
  That's because the `setup` block is checking out the connection only
 | 
						|
  for the test process. Once the worker attempts to perform a query,
 | 
						|
  there is no connection assigned to it and it will fail.
 | 
						|
 | 
						|
  The sandbox module provides two ways of doing so, via allowances or
 | 
						|
  by running in shared mode.
 | 
						|
 | 
						|
  ### Allowances
 | 
						|
 | 
						|
  The idea behind allowances is that you can explicitly tell a process
 | 
						|
  which checked out connection it should use, allowing multiple processes
 | 
						|
  to collaborate over the same connection. Let's give it a try:
 | 
						|
 | 
						|
      test "calls worker that runs a query" do
 | 
						|
        allow = Process.whereis(MyApp.Worker)
 | 
						|
        Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), allow)
 | 
						|
        GenServer.call(MyApp.Worker, :run_query)
 | 
						|
      end
 | 
						|
 | 
						|
  And that's it, by calling `allow/3`, we are explicitly assigning
 | 
						|
  the parent's connection (i.e. the test process' connection) to
 | 
						|
  the task.
 | 
						|
 | 
						|
  Because allowances use an explicit mechanism, their advantage
 | 
						|
  is that you can still run your tests in async mode. The downside
 | 
						|
  is that you need to explicitly control and allow every single
 | 
						|
  process. This is not always possible. In such cases, you will
 | 
						|
  want to use shared mode.
 | 
						|
 | 
						|
  ### Shared mode
 | 
						|
 | 
						|
  Shared mode allows a process to share its connection with any other
 | 
						|
  process automatically, without relying on explicit allowances.
 | 
						|
  Let's change the example above to use shared mode:
 | 
						|
 | 
						|
      setup do
 | 
						|
        # Explicitly get a connection before each test
 | 
						|
        :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
 | 
						|
        # Setting the shared mode must be done only after checkout
 | 
						|
        Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
 | 
						|
      end
 | 
						|
 | 
						|
      test "calls worker that runs a query" do
 | 
						|
        GenServer.call(MyApp.Worker, :run_query)
 | 
						|
      end
 | 
						|
 | 
						|
  By calling `mode({:shared, self()})`, any process that needs
 | 
						|
  to talk to the database will now use the same connection as the
 | 
						|
  one checked out by the test process during the `setup` block.
 | 
						|
 | 
						|
  Make sure to always check a connection out before setting the mode
 | 
						|
  to `{:shared, self()}`.
 | 
						|
 | 
						|
  The advantage of shared mode is that by calling a single function,
 | 
						|
  you will ensure all upcoming processes and operations will use that
 | 
						|
  shared connection, without a need to explicitly allow them. The
 | 
						|
  downside is that tests can no longer run concurrently in shared mode.
 | 
						|
 | 
						|
  Also, beware that if the test process terminates while the worker is
 | 
						|
  using the connection, the connection will be taken away from the worker,
 | 
						|
  which will error. Therefore it is important to guarantee the work is done
 | 
						|
  before the test concludes. In the example above, we are using a `call`,
 | 
						|
  which is synchronous, avoiding the problem, but you may need to explicitly
 | 
						|
  flush the worker or terminate it under such scenarios in your tests.
 | 
						|
 | 
						|
  ### Summing up
 | 
						|
 | 
						|
  There are two mechanisms for explicit ownerships:
 | 
						|
 | 
						|
    * Using allowances - requires explicit allowances via `allow/3`.
 | 
						|
      Tests may run concurrently.
 | 
						|
 | 
						|
    * Using shared mode - does not require explicit allowances.
 | 
						|
      Tests cannot run concurrently.
 | 
						|
 | 
						|
  ## FAQ
 | 
						|
 | 
						|
  When running the sandbox mode concurrently, developers may run into
 | 
						|
  issues we explore in the upcoming sections.
 | 
						|
 | 
						|
  ### "owner exited"
 | 
						|
 | 
						|
  In some situations, you may see error reports similar to the one below:
 | 
						|
 | 
						|
      23:59:59.999 [error] Postgrex.Protocol (#PID<>) disconnected:
 | 
						|
          ** (DBConnection.Error) owner #PID<> exited
 | 
						|
      Client #PID<> is still using a connection from owner
 | 
						|
 | 
						|
  Such errors are usually followed by another error report from another
 | 
						|
  process that failed while executing a database query.
 | 
						|
 | 
						|
  To understand the failure, we need to answer the question: who are the
 | 
						|
  owner and client processes? The owner process is the one that checks
 | 
						|
  out the connection, which, in the majority of cases, is the test process,
 | 
						|
  the one running your tests. In other words, the error happens because
 | 
						|
  the test process has finished, either because the test succeeded or
 | 
						|
  because it failed, while the client process was trying to get information
 | 
						|
  from the database. Since the owner process, the one that owns the
 | 
						|
  connection, no longer exists, Ecto will check the connection back in
 | 
						|
  and notify the client process using the connection that the connection
 | 
						|
  owner is no longer available.
 | 
						|
 | 
						|
  This can happen in different situations. For example, imagine you query
 | 
						|
  a GenServer in your test that is using a database connection:
 | 
						|
 | 
						|
      test "gets results from GenServer" do
 | 
						|
        {:ok, pid} = MyAppServer.start_link()
 | 
						|
        Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
 | 
						|
        assert MyAppServer.get_my_data_fast(timeout: 1000) == [...]
 | 
						|
      end
 | 
						|
 | 
						|
  In the test above, we spawn the server and allow it to perform database
 | 
						|
  queries using the connection owned by the test process. Since we gave
 | 
						|
  a timeout of 1 second, in case the database takes longer than one second
 | 
						|
  to reply, the test process will fail, due to the timeout, making the
 | 
						|
  "owner down" message to be printed because the server process is still
 | 
						|
  waiting on a connection reply.
 | 
						|
 | 
						|
  In some situations, such failures may be intermittent. Imagine that you
 | 
						|
  allow a process that queries the database every half second:
 | 
						|
 | 
						|
      test "queries periodically" do
 | 
						|
        {:ok, pid} = PeriodicServer.start_link()
 | 
						|
        Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
 | 
						|
        # assertions
 | 
						|
      end
 | 
						|
 | 
						|
  Because the server is querying the database from time to time, there is
 | 
						|
  a chance that, when the test exits, the periodic process may be querying
 | 
						|
  the database, regardless of test success or failure.
 | 
						|
 | 
						|
  To address this, you can tell ExUnit to manage your processes:
 | 
						|
 | 
						|
      test "queries periodically" do
 | 
						|
        pid = start_supervised!(PeriodicServer)
 | 
						|
        Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
 | 
						|
        # assertions
 | 
						|
      end
 | 
						|
 | 
						|
  By using `start_supervised!/1`, ExUnit guarantees the process finishes
 | 
						|
  before your test (the connection owner).
 | 
						|
 | 
						|
  In some situations, however, the dynamic processes are directly started
 | 
						|
  inside a `DynamicSupervisor` or a `Task.Supervisor`. You can guarantee
 | 
						|
  proper termination in such scenarios by adding an `on_exit` callback
 | 
						|
  that waits until all supervised children terminate:
 | 
						|
 | 
						|
      on_exit(fn ->
 | 
						|
        for {_, pid, _, _} <- DynamicSupervisor.which_children(MyApp.DynamicSupervisor) do
 | 
						|
          ref = Process.monitor(pid)
 | 
						|
          assert_receive {:DOWN, ^ref, _, _, _}, :infinity
 | 
						|
        end
 | 
						|
      end)
 | 
						|
 | 
						|
  ### "owner timed out because it owned the connection for longer than Nms"
 | 
						|
 | 
						|
  In some situations, you may see error reports similar to the one below:
 | 
						|
 | 
						|
      09:56:43.081 [error] Postgrex.Protocol (#PID<>) disconnected:
 | 
						|
          ** (DBConnection.ConnectionError) owner #PID<> timed out
 | 
						|
          because it owned the connection for longer than 120000ms
 | 
						|
 | 
						|
  If you have a long running test (or you're debugging with IEx.pry),
 | 
						|
  the timeout for the connection ownership may be too short.  You can
 | 
						|
  increase the timeout by setting the `:ownership_timeout` options for
 | 
						|
  your repo config in `config/config.exs` (or preferably in `config/test.exs`):
 | 
						|
 | 
						|
      config :my_app, MyApp.Repo,
 | 
						|
        ownership_timeout: NEW_TIMEOUT_IN_MILLISECONDS
 | 
						|
 | 
						|
  The `:ownership_timeout` option is part of `DBConnection.Ownership`
 | 
						|
  and defaults to 120000ms. Timeouts are given as integers in milliseconds.
 | 
						|
 | 
						|
  Alternately, if this is an issue for only a handful of long-running tests,
 | 
						|
  you can pass an `:ownership_timeout` option when calling
 | 
						|
  `Ecto.Adapters.SQL.Sandbox.checkout/2` instead of setting a longer timeout
 | 
						|
  globally in your config.
 | 
						|
 | 
						|
  ### Deferred constraints
 | 
						|
 | 
						|
  Some databases allow to defer constraint validation to the transaction
 | 
						|
  commit time, instead of the particular statement execution time. This
 | 
						|
  feature, for instance, allows for a cyclic foreign key referencing.
 | 
						|
  Since the SQL Sandbox mode rolls back transactions, tests might report
 | 
						|
  false positives because deferred constraints are never checked by the
 | 
						|
  database. To manually force deferred constraints validation when using
 | 
						|
  PostgreSQL use the following line right at the end of your test case:
 | 
						|
 | 
						|
      Repo.query!("SET CONSTRAINTS ALL IMMEDIATE")
 | 
						|
 | 
						|
  ### Database locks and deadlocks
 | 
						|
 | 
						|
  Since the sandbox relies on concurrent transactional tests, there is
 | 
						|
  a chance your tests may trigger deadlocks in your database. This is
 | 
						|
  specially true with MySQL, where the solutions presented here are not
 | 
						|
  enough to avoid deadlocks and therefore making the use of concurrent tests
 | 
						|
  with MySQL prohibited.
 | 
						|
 | 
						|
  However, even on databases like PostgreSQL, performance degradations or
 | 
						|
  deadlocks may still occur. For example, imagine a "users" table with a
 | 
						|
  unique index on the "email" column. Now consider multiple tests are
 | 
						|
  trying to insert the same user email to the database. They will attempt
 | 
						|
  to retrieve the same database lock, causing only one test to succeed and
 | 
						|
  run while all other tests wait for the lock.
 | 
						|
 | 
						|
  In other situations, two different tests may proceed in a way that
 | 
						|
  each test retrieves locks desired by the other, leading to a situation
 | 
						|
  that cannot be resolved, a deadlock. For instance:
 | 
						|
 | 
						|
  ```text
 | 
						|
  Transaction 1:                Transaction 2:
 | 
						|
  begin
 | 
						|
                                begin
 | 
						|
  update posts where id = 1
 | 
						|
                                update posts where id = 2
 | 
						|
                                update posts where id = 1
 | 
						|
  update posts where id = 2
 | 
						|
                        **deadlock**
 | 
						|
  ```
 | 
						|
 | 
						|
  There are different ways to avoid such problems. One of them is
 | 
						|
  to make sure your tests work on distinct data. Regardless of
 | 
						|
  your choice between using fixtures or factories for test data,
 | 
						|
  make sure you get a new set of data per test. This is specially
 | 
						|
  important for data that is meant to be unique like user emails.
 | 
						|
 | 
						|
  For example, instead of:
 | 
						|
 | 
						|
      def insert_user do
 | 
						|
        Repo.insert!(%User{email: "sample@example.com"})
 | 
						|
      end
 | 
						|
 | 
						|
  prefer:
 | 
						|
 | 
						|
      def insert_user do
 | 
						|
        Repo.insert!(%User{email: "sample-#{counter()}@example.com"})
 | 
						|
      end
 | 
						|
 | 
						|
      defp counter do
 | 
						|
        System.unique_integer([:positive])
 | 
						|
      end
 | 
						|
 | 
						|
  In fact, avoiding unique emails like above can also have a positive
 | 
						|
  impact on the test suite performance, as it reduces contention and
 | 
						|
  wait between concurrent tests. We have heard reports where using
 | 
						|
  dynamic values for uniquely indexed columns, as we did for email
 | 
						|
  above, made a test suite run between 2x to 3x faster.
 | 
						|
 | 
						|
  Deadlocks may happen in other circumstances. If you believe you
 | 
						|
  are hitting a scenario that has not been described here, please
 | 
						|
  report an issue so we can improve our examples. As a last resort,
 | 
						|
  you can always disable the test triggering the deadlock from
 | 
						|
  running asynchronously by setting  "async: false".
 | 
						|
  """
 | 
						|
 | 
						|
  defmodule Connection do
 | 
						|
    @moduledoc false
 | 
						|
    if Code.ensure_loaded?(DBConnection) do
 | 
						|
      @behaviour DBConnection
 | 
						|
    end
 | 
						|
 | 
						|
    def connect(_opts) do
 | 
						|
      raise "should never be invoked"
 | 
						|
    end
 | 
						|
 | 
						|
    def disconnect(err, {conn_mod, state, _in_transaction?}) do
 | 
						|
      conn_mod.disconnect(err, state)
 | 
						|
    end
 | 
						|
 | 
						|
    def checkout(state), do: proxy(:checkout, state, [])
 | 
						|
    def checkin(state), do: proxy(:checkin, state, [])
 | 
						|
    def ping(state), do: proxy(:ping, state, [])
 | 
						|
 | 
						|
    def handle_begin(opts, {conn_mod, state, false}) do
 | 
						|
      opts = [mode: :savepoint] ++ opts
 | 
						|
 | 
						|
      case conn_mod.handle_begin(opts, state) do
 | 
						|
        {:ok, value, state} ->
 | 
						|
          {:ok, value, {conn_mod, state, true}}
 | 
						|
 | 
						|
        {kind, err, state} ->
 | 
						|
          {kind, err, {conn_mod, state, false}}
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def handle_commit(opts, {conn_mod, state, true}) do
 | 
						|
      opts = [mode: :savepoint] ++ opts
 | 
						|
      proxy(:handle_commit, {conn_mod, state, false}, [opts])
 | 
						|
    end
 | 
						|
 | 
						|
    def handle_rollback(opts, {conn_mod, state, _}) do
 | 
						|
      opts = [mode: :savepoint] ++ opts
 | 
						|
      proxy(:handle_rollback, {conn_mod, state, false}, [opts])
 | 
						|
    end
 | 
						|
 | 
						|
    def handle_status(opts, state),
 | 
						|
      do: proxy(:handle_status, state, [maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    def handle_prepare(query, opts, state),
 | 
						|
      do: proxy(:handle_prepare, state, [query, maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    def handle_execute(query, params, opts, state),
 | 
						|
      do: proxy(:handle_execute, state, [query, params, maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    def handle_close(query, opts, state),
 | 
						|
      do: proxy(:handle_close, state, [query, maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    def handle_declare(query, params, opts, state),
 | 
						|
      do: proxy(:handle_declare, state, [query, params, maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    def handle_fetch(query, cursor, opts, state),
 | 
						|
      do: proxy(:handle_fetch, state, [query, cursor, maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    def handle_deallocate(query, cursor, opts, state),
 | 
						|
      do: proxy(:handle_deallocate, state, [query, cursor, maybe_savepoint(opts, state)])
 | 
						|
 | 
						|
    defp maybe_savepoint(opts, {_, _, in_transaction?}) do
 | 
						|
      if not in_transaction? and Keyword.get(opts, :sandbox_subtransaction, true) do
 | 
						|
        [mode: :savepoint] ++ opts
 | 
						|
      else
 | 
						|
        opts
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    defp proxy(fun, {conn_mod, state, in_transaction?}, args) do
 | 
						|
      result = apply(conn_mod, fun, args ++ [state])
 | 
						|
      pos = :erlang.tuple_size(result)
 | 
						|
      :erlang.setelement(pos, result, {conn_mod, :erlang.element(pos, result), in_transaction?})
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Starts a process that owns the connection and returns its pid.
 | 
						|
 | 
						|
  The owner process is not linked to the caller, it is your responsibility to
 | 
						|
  ensure it will be stopped. In tests, this is done by terminating the pool
 | 
						|
  in an `ExUnit.Callbacks.on_exit/2` callback:
 | 
						|
 | 
						|
      setup tags do
 | 
						|
        pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
 | 
						|
        on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
 | 
						|
        :ok
 | 
						|
      end
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
    * `:shared` - if `true`, the pool runs in the shared mode. Defaults to `false`
 | 
						|
 | 
						|
  The remaining options are passed to `checkout/2`.
 | 
						|
  """
 | 
						|
  @doc since: "3.4.4"
 | 
						|
  @spec start_owner!(Ecto.Repo.t() | pid(), keyword()) :: pid()
 | 
						|
  def start_owner!(repo, opts \\ []) do
 | 
						|
    parent = self()
 | 
						|
 | 
						|
    {:ok, pid} =
 | 
						|
      Agent.start(fn ->
 | 
						|
        {shared, opts} = Keyword.pop(opts, :shared, false)
 | 
						|
        :ok = checkout(repo, opts)
 | 
						|
 | 
						|
        if shared do
 | 
						|
          :ok = mode(repo, {:shared, self()})
 | 
						|
        else
 | 
						|
          :ok = allow(repo, self(), parent)
 | 
						|
        end
 | 
						|
      end)
 | 
						|
 | 
						|
    pid
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Stops an owner process started by `start_owner!/2`.
 | 
						|
  """
 | 
						|
  @doc since: "3.4.4"
 | 
						|
  @spec stop_owner(pid()) :: :ok
 | 
						|
  def stop_owner(pid) do
 | 
						|
    GenServer.stop(pid)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Sets the mode for the `repo` pool.
 | 
						|
 | 
						|
  The modes can be:
 | 
						|
 | 
						|
    * `:auto` - this is the default mode. When trying to use the repository,
 | 
						|
      processes can automatically checkout a connection without calling
 | 
						|
      `checkout/2` or `start_owner/2` before. This is the mode you will run
 | 
						|
      on before your test suite starts
 | 
						|
 | 
						|
    * `:manual` - in this mode, the connection always has to be explicitly
 | 
						|
      checked before used. Other processes are allowed to use the same
 | 
						|
      connection if they are explicitly allowed via `allow/4`. You usually
 | 
						|
      set the mode to manual at the end of your `test/test_helper.exs` file.
 | 
						|
      This is also the mode you will run your async tests in
 | 
						|
 | 
						|
    * `{:shared, pid}` - after checking out a connection in manual mode,
 | 
						|
      you can change the mode to `{:shared, pid}`, where pid is the process
 | 
						|
      that owns the connection, most often `{:shared, self()}`. This makes it
 | 
						|
      so all processes can use the same connection as the one owned by the
 | 
						|
      current process. This is the mode you will run your sync tests in
 | 
						|
 | 
						|
  Whenever you change the mode to `:manual` or `:auto`, all existing
 | 
						|
  connections are checked in. Therefore, it is recommend to set those
 | 
						|
  modes before your test suite starts, as otherwise you will check in
 | 
						|
  connections being used in any other test running concurrently.
 | 
						|
 | 
						|
  If successful, returns `:ok` (this is always successful for `:auto`
 | 
						|
  and `:manual` modes). It may return `:not_owner` or `:not_found`
 | 
						|
  when setting `{:shared, pid}` and the given `pid` does not own any
 | 
						|
  connection for the repo. May return `:already_shared` if another
 | 
						|
  process set the ownership mode to `{:shared, _}` and is still alive.
 | 
						|
  """
 | 
						|
  @spec mode(Ecto.Repo.t() | pid(), :auto | :manual | {:shared, pid()}) ::
 | 
						|
          :ok | :already_shared | :now_owner | :not_found
 | 
						|
  def mode(repo, mode)
 | 
						|
      when (is_atom(repo) or is_pid(repo)) and mode in [:auto, :manual]
 | 
						|
      when (is_atom(repo) or is_pid(repo)) and elem(mode, 0) == :shared and is_pid(elem(mode, 1)) do
 | 
						|
    %{pid: pool, opts: opts} = lookup_meta!(repo)
 | 
						|
    DBConnection.Ownership.ownership_mode(pool, mode, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Checks a connection out for the given `repo`.
 | 
						|
 | 
						|
  The process calling `checkout/2` will own the connection
 | 
						|
  until it calls `checkin/2` or until it crashes in which case
 | 
						|
  the connection will be automatically reclaimed by the pool.
 | 
						|
 | 
						|
  If successful, returns `:ok`. If the caller already has a
 | 
						|
  connection, it returns `{:already, :owner | :allowed}`.
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
    * `:sandbox` - when true the connection is wrapped in
 | 
						|
      a transaction. Defaults to true.
 | 
						|
 | 
						|
    * `:isolation` - set the query to the given isolation level.
 | 
						|
 | 
						|
    * `:ownership_timeout` - limits how long the connection can be
 | 
						|
      owned. Defaults to the value in your repo config in
 | 
						|
      `config/config.exs` (or preferably in `config/test.exs`), or
 | 
						|
      120000 ms if not set. The timeout exists for sanity checking
 | 
						|
      purposes, to ensure there is no connection leakage, and can
 | 
						|
      be bumped whenever necessary.
 | 
						|
 | 
						|
  """
 | 
						|
  @spec checkout(Ecto.Repo.t() | pid(), keyword()) :: :ok | {:already, :owner | :allowed}
 | 
						|
  def checkout(repo, opts \\ []) when is_atom(repo) or is_pid(repo) do
 | 
						|
    %{pid: pool, opts: pool_opts} = lookup_meta!(repo)
 | 
						|
 | 
						|
    pool_opts =
 | 
						|
      if Keyword.get(opts, :sandbox, true) do
 | 
						|
        [
 | 
						|
          post_checkout: &post_checkout(&1, &2, opts),
 | 
						|
          pre_checkin: &pre_checkin(&1, &2, &3, opts)
 | 
						|
        ] ++ pool_opts
 | 
						|
      else
 | 
						|
        pool_opts
 | 
						|
      end
 | 
						|
 | 
						|
    pool_opts_overrides = Keyword.take(opts, [:ownership_timeout, :isolation_level])
 | 
						|
    pool_opts = Keyword.merge(pool_opts, pool_opts_overrides)
 | 
						|
 | 
						|
    case DBConnection.Ownership.ownership_checkout(pool, pool_opts) do
 | 
						|
      :ok ->
 | 
						|
        if isolation = opts[:isolation] do
 | 
						|
          set_transaction_isolation_level(repo, isolation)
 | 
						|
        end
 | 
						|
 | 
						|
        :ok
 | 
						|
 | 
						|
      other ->
 | 
						|
        other
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp set_transaction_isolation_level(repo, isolation) do
 | 
						|
    query = "SET TRANSACTION ISOLATION LEVEL #{isolation}"
 | 
						|
 | 
						|
    case Ecto.Adapters.SQL.query(repo, query, [], sandbox_subtransaction: false) do
 | 
						|
      {:ok, _} ->
 | 
						|
        :ok
 | 
						|
 | 
						|
      {:error, error} ->
 | 
						|
        checkin(repo, [])
 | 
						|
        raise error
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Checks in the connection back into the sandbox pool.
 | 
						|
  """
 | 
						|
  @spec checkin(Ecto.Repo.t() | pid()) :: :ok | :not_owner | :not_found
 | 
						|
  def checkin(repo, _opts \\ []) when is_atom(repo) or is_pid(repo) do
 | 
						|
    %{pid: pool, opts: opts} = lookup_meta!(repo)
 | 
						|
    DBConnection.Ownership.ownership_checkin(pool, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Allows the `allow` process to use the same connection as `parent`.
 | 
						|
 | 
						|
  `allow` may be a PID or a locally registered name.
 | 
						|
 | 
						|
  If the allowance is successful, this function returns `:ok`. If `allow` is already an
 | 
						|
  owner or already allowed, it returns `{:already, :owner | :allowed}`. If `parent` has not
 | 
						|
  checked out a connection from the repo, it returns `:not_found`.
 | 
						|
  """
 | 
						|
  @spec allow(Ecto.Repo.t() | pid(), pid(), term()) ::
 | 
						|
          :ok | {:already, :owner | :allowed} | :not_found
 | 
						|
  def allow(repo, parent, allow, _opts \\ []) when is_atom(repo) or is_pid(repo) do
 | 
						|
    case GenServer.whereis(allow) do
 | 
						|
      pid when is_pid(pid) ->
 | 
						|
        %{pid: pool, opts: opts} = lookup_meta!(repo)
 | 
						|
        DBConnection.Ownership.ownership_allow(pool, parent, pid, opts)
 | 
						|
 | 
						|
      other ->
 | 
						|
        raise """
 | 
						|
        only PID or a locally registered process can be allowed to \
 | 
						|
        use the same connection as parent but the lookup returned #{inspect(other)}
 | 
						|
        """
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Runs a function outside of the sandbox.
 | 
						|
  """
 | 
						|
  @spec unboxed_run(Ecto.Repo.t() | pid(), (-> result)) :: result when result: var
 | 
						|
  def unboxed_run(repo, fun) when is_atom(repo) or is_pid(repo) do
 | 
						|
    checkin(repo)
 | 
						|
    checkout(repo, sandbox: false)
 | 
						|
 | 
						|
    try do
 | 
						|
      fun.()
 | 
						|
    after
 | 
						|
      checkin(repo)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp lookup_meta!(repo) do
 | 
						|
    %{opts: opts} =
 | 
						|
      meta =
 | 
						|
      repo
 | 
						|
      |> find_repo()
 | 
						|
      |> Ecto.Adapter.lookup_meta()
 | 
						|
 | 
						|
    if opts[:pool] != DBConnection.Ownership do
 | 
						|
      raise """
 | 
						|
      cannot invoke sandbox operation with pool #{inspect(opts[:pool])}.
 | 
						|
      To use the SQL Sandbox, configure your repository pool as:
 | 
						|
 | 
						|
          pool: #{inspect(__MODULE__)}
 | 
						|
      """
 | 
						|
    end
 | 
						|
 | 
						|
    meta
 | 
						|
  end
 | 
						|
 | 
						|
  defp find_repo(repo) when is_atom(repo), do: repo.get_dynamic_repo()
 | 
						|
  defp find_repo(repo), do: repo
 | 
						|
 | 
						|
  defp post_checkout(conn_mod, conn_state, opts) do
 | 
						|
    case conn_mod.handle_begin([mode: :transaction] ++ opts, conn_state) do
 | 
						|
      {:ok, _, conn_state} ->
 | 
						|
        {:ok, Connection, {conn_mod, conn_state, false}}
 | 
						|
 | 
						|
      {:transaction, _conn_state} ->
 | 
						|
        raise """
 | 
						|
        Ecto SQL sandbox transaction cannot be started because there is already\
 | 
						|
        a transaction running.
 | 
						|
 | 
						|
        This either means some code is starting a transaction before the sandbox\
 | 
						|
        or a connection was not appropriately rolled back after use.
 | 
						|
        """
 | 
						|
 | 
						|
      {_error_or_disconnect, err, conn_state} ->
 | 
						|
        {:disconnect, err, conn_mod, conn_state}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp pre_checkin(:checkin, Connection, {conn_mod, conn_state, _in_transaction?}, opts) do
 | 
						|
    case conn_mod.handle_rollback([mode: :transaction] ++ opts, conn_state) do
 | 
						|
      {:ok, _, conn_state} ->
 | 
						|
        {:ok, conn_mod, conn_state}
 | 
						|
 | 
						|
      {:idle, _conn_state} ->
 | 
						|
        raise """
 | 
						|
        Ecto SQL sandbox transaction was already committed/rolled back.
 | 
						|
 | 
						|
        The sandbox works by running each test in a transaction and closing the\
 | 
						|
        transaction afterwards. However, the transaction has already terminated.\
 | 
						|
        Your test code is likely committing or rolling back transactions manually,\
 | 
						|
        either by invoking procedures or running custom SQL commands.
 | 
						|
 | 
						|
        One option is to manually checkout a connection without a sandbox:
 | 
						|
 | 
						|
            Ecto.Adapters.SQL.Sandbox.checkout(repo, sandbox: false)
 | 
						|
 | 
						|
        But remember you will have to undo any database changes performed by such tests.
 | 
						|
        """
 | 
						|
 | 
						|
      {_error_or_disconnect, err, conn_state} ->
 | 
						|
        {:disconnect, err, conn_mod, conn_state}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp pre_checkin(_, Connection, {conn_mod, conn_state, _in_transaction?}, _opts) do
 | 
						|
    {:ok, conn_mod, conn_state}
 | 
						|
  end
 | 
						|
end
 |