zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

sandbox.ex (23049B)


      1 defmodule Ecto.Adapters.SQL.Sandbox do
      2   @moduledoc ~S"""
      3   A pool for concurrent transactional tests.
      4 
      5   The sandbox pool is implemented on top of an ownership mechanism.
      6   When started, the pool is in automatic mode, which means the
      7   repository will automatically check connections out as with any
      8   other pool.
      9 
     10   The `mode/2` function can be used to change the pool mode from
     11   automatic to either manual or shared. In the latter two modes,
     12   the connection must be explicitly checked out before use.
     13   When explicit checkouts are made, the sandbox will wrap the
     14   connection in a transaction by default and control who has
     15   access to it. This means developers have a safe mechanism for
     16   running concurrent tests against the database.
     17 
     18   ## Database support
     19 
     20   While both PostgreSQL and MySQL support SQL Sandbox, only PostgreSQL
     21   supports concurrent tests while running the SQL Sandbox. Therefore, do
     22   not run concurrent tests with MySQL as you may run into deadlocks due to
     23   its transaction implementation.
     24 
     25   ## Example
     26 
     27   The first step is to configure your database to use the
     28   `Ecto.Adapters.SQL.Sandbox` pool. You set those options in your
     29   `config/config.exs` (or preferably `config/test.exs`) if you
     30   haven't yet:
     31 
     32       config :my_app, Repo,
     33         pool: Ecto.Adapters.SQL.Sandbox
     34 
     35   Now with the test database properly configured, you can write
     36   transactional tests:
     37 
     38       # At the end of your test_helper.exs
     39       # Set the pool mode to manual for explicit checkouts
     40       Ecto.Adapters.SQL.Sandbox.mode(Repo, :manual)
     41 
     42       defmodule PostTest do
     43         # Once the mode is manual, tests can also be async
     44         use ExUnit.Case, async: true
     45 
     46         setup do
     47           # Explicitly get a connection before each test
     48           :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
     49         end
     50 
     51         test "create post" do
     52           # Use the repository as usual
     53           assert %Post{} = Repo.insert!(%Post{})
     54         end
     55       end
     56 
     57   ## Collaborating processes
     58 
     59   The example above is straight-forward because we have only
     60   a single process using the database connection. However,
     61   sometimes a test may need to interact with multiple processes,
     62   all using the same connection so they all belong to the same
     63   transaction.
     64 
     65   Before we discuss solutions, let's see what happens if we try
     66   to use a connection from a new process without explicitly
     67   checking it out first:
     68 
     69       setup do
     70         # Explicitly get a connection before each test
     71         :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
     72       end
     73 
     74       test "calls worker that runs a query" do
     75         GenServer.call(MyApp.Worker, :run_query)
     76       end
     77 
     78   The test above will fail with an error similar to:
     79 
     80       ** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.35.0>
     81 
     82   That's because the `setup` block is checking out the connection only
     83   for the test process. Once the worker attempts to perform a query,
     84   there is no connection assigned to it and it will fail.
     85 
     86   The sandbox module provides two ways of doing so, via allowances or
     87   by running in shared mode.
     88 
     89   ### Allowances
     90 
     91   The idea behind allowances is that you can explicitly tell a process
     92   which checked out connection it should use, allowing multiple processes
     93   to collaborate over the same connection. Let's give it a try:
     94 
     95       test "calls worker that runs a query" do
     96         allow = Process.whereis(MyApp.Worker)
     97         Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), allow)
     98         GenServer.call(MyApp.Worker, :run_query)
     99       end
    100 
    101   And that's it, by calling `allow/3`, we are explicitly assigning
    102   the parent's connection (i.e. the test process' connection) to
    103   the task.
    104 
    105   Because allowances use an explicit mechanism, their advantage
    106   is that you can still run your tests in async mode. The downside
    107   is that you need to explicitly control and allow every single
    108   process. This is not always possible. In such cases, you will
    109   want to use shared mode.
    110 
    111   ### Shared mode
    112 
    113   Shared mode allows a process to share its connection with any other
    114   process automatically, without relying on explicit allowances.
    115   Let's change the example above to use shared mode:
    116 
    117       setup do
    118         # Explicitly get a connection before each test
    119         :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
    120         # Setting the shared mode must be done only after checkout
    121         Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
    122       end
    123 
    124       test "calls worker that runs a query" do
    125         GenServer.call(MyApp.Worker, :run_query)
    126       end
    127 
    128   By calling `mode({:shared, self()})`, any process that needs
    129   to talk to the database will now use the same connection as the
    130   one checked out by the test process during the `setup` block.
    131 
    132   Make sure to always check a connection out before setting the mode
    133   to `{:shared, self()}`.
    134 
    135   The advantage of shared mode is that by calling a single function,
    136   you will ensure all upcoming processes and operations will use that
    137   shared connection, without a need to explicitly allow them. The
    138   downside is that tests can no longer run concurrently in shared mode.
    139 
    140   Also, beware that if the test process terminates while the worker is
    141   using the connection, the connection will be taken away from the worker,
    142   which will error. Therefore it is important to guarantee the work is done
    143   before the test concludes. In the example above, we are using a `call`,
    144   which is synchronous, avoiding the problem, but you may need to explicitly
    145   flush the worker or terminate it under such scenarios in your tests.
    146 
    147   ### Summing up
    148 
    149   There are two mechanisms for explicit ownerships:
    150 
    151     * Using allowances - requires explicit allowances via `allow/3`.
    152       Tests may run concurrently.
    153 
    154     * Using shared mode - does not require explicit allowances.
    155       Tests cannot run concurrently.
    156 
    157   ## FAQ
    158 
    159   When running the sandbox mode concurrently, developers may run into
    160   issues we explore in the upcoming sections.
    161 
    162   ### "owner exited"
    163 
    164   In some situations, you may see error reports similar to the one below:
    165 
    166       23:59:59.999 [error] Postgrex.Protocol (#PID<>) disconnected:
    167           ** (DBConnection.Error) owner #PID<> exited
    168       Client #PID<> is still using a connection from owner
    169 
    170   Such errors are usually followed by another error report from another
    171   process that failed while executing a database query.
    172 
    173   To understand the failure, we need to answer the question: who are the
    174   owner and client processes? The owner process is the one that checks
    175   out the connection, which, in the majority of cases, is the test process,
    176   the one running your tests. In other words, the error happens because
    177   the test process has finished, either because the test succeeded or
    178   because it failed, while the client process was trying to get information
    179   from the database. Since the owner process, the one that owns the
    180   connection, no longer exists, Ecto will check the connection back in
    181   and notify the client process using the connection that the connection
    182   owner is no longer available.
    183 
    184   This can happen in different situations. For example, imagine you query
    185   a GenServer in your test that is using a database connection:
    186 
    187       test "gets results from GenServer" do
    188         {:ok, pid} = MyAppServer.start_link()
    189         Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
    190         assert MyAppServer.get_my_data_fast(timeout: 1000) == [...]
    191       end
    192 
    193   In the test above, we spawn the server and allow it to perform database
    194   queries using the connection owned by the test process. Since we gave
    195   a timeout of 1 second, in case the database takes longer than one second
    196   to reply, the test process will fail, due to the timeout, making the
    197   "owner down" message to be printed because the server process is still
    198   waiting on a connection reply.
    199 
    200   In some situations, such failures may be intermittent. Imagine that you
    201   allow a process that queries the database every half second:
    202 
    203       test "queries periodically" do
    204         {:ok, pid} = PeriodicServer.start_link()
    205         Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
    206         # more tests
    207       end
    208 
    209   Because the server is querying the database from time to time, there is
    210   a chance that, when the test exits, the periodic process may be querying
    211   the database, regardless of test success or failure.
    212 
    213   ### "owner timed out because it owned the connection for longer than Nms"
    214 
    215   In some situations, you may see error reports similar to the one below:
    216 
    217       09:56:43.081 [error] Postgrex.Protocol (#PID<>) disconnected:
    218           ** (DBConnection.ConnectionError) owner #PID<> timed out
    219           because it owned the connection for longer than 120000ms
    220 
    221   If you have a long running test (or you're debugging with IEx.pry),
    222   the timeout for the connection ownership may be too short.  You can
    223   increase the timeout by setting the `:ownership_timeout` options for
    224   your repo config in `config/config.exs` (or preferably in `config/test.exs`):
    225 
    226       config :my_app, MyApp.Repo,
    227         ownership_timeout: NEW_TIMEOUT_IN_MILLISECONDS
    228 
    229   The `:ownership_timeout` option is part of `DBConnection.Ownership`
    230   and defaults to 120000ms. Timeouts are given as integers in milliseconds.
    231 
    232   Alternately, if this is an issue for only a handful of long-running tests,
    233   you can pass an `:ownership_timeout` option when calling
    234   `Ecto.Adapters.SQL.Sandbox.checkout/2` instead of setting a longer timeout
    235   globally in your config.
    236 
    237   ### Deferred constraints
    238 
    239   Some databases allow to defer constraint validation to the transaction
    240   commit time, instead of the particular statement execution time. This
    241   feature, for instance, allows for a cyclic foreign key referencing.
    242   Since the SQL Sandbox mode rolls back transactions, tests might report
    243   false positives because deferred constraints are never checked by the
    244   database. To manually force deferred constraints validation when using
    245   PostgreSQL use the following line right at the end of your test case:
    246 
    247       Repo.query!("SET CONSTRAINTS ALL IMMEDIATE")
    248 
    249   ### Database locks and deadlocks
    250 
    251   Since the sandbox relies on concurrent transactional tests, there is
    252   a chance your tests may trigger deadlocks in your database. This is
    253   specially true with MySQL, where the solutions presented here are not
    254   enough to avoid deadlocks and therefore making the use of concurrent tests
    255   with MySQL prohibited.
    256 
    257   However, even on databases like PostgreSQL, performance degradations or
    258   deadlocks may still occur. For example, imagine a "users" table with a
    259   unique index on the "email" column. Now consider multiple tests are
    260   trying to insert the same user email to the database. They will attempt
    261   to retrieve the same database lock, causing only one test to succeed and
    262   run while all other tests wait for the lock.
    263 
    264   In other situations, two different tests may proceed in a way that
    265   each test retrieves locks desired by the other, leading to a situation
    266   that cannot be resolved, a deadlock. For instance:
    267 
    268   ```text
    269   Transaction 1:                Transaction 2:
    270   begin
    271                                 begin
    272   update posts where id = 1
    273                                 update posts where id = 2
    274                                 update posts where id = 1
    275   update posts where id = 2
    276                         **deadlock**
    277   ```
    278 
    279   There are different ways to avoid such problems. One of them is
    280   to make sure your tests work on distinct data. Regardless of
    281   your choice between using fixtures or factories for test data,
    282   make sure you get a new set of data per test. This is specially
    283   important for data that is meant to be unique like user emails.
    284 
    285   For example, instead of:
    286 
    287       def insert_user do
    288         Repo.insert!(%User{email: "sample@example.com"})
    289       end
    290 
    291   prefer:
    292 
    293       def insert_user do
    294         Repo.insert!(%User{email: "sample-#{counter()}@example.com"})
    295       end
    296 
    297       defp counter do
    298         System.unique_integer([:positive])
    299       end
    300 
    301   In fact, avoiding unique emails like above can also have a positive
    302   impact on the test suite performance, as it reduces contention and
    303   wait between concurrent tests. We have heard reports where using
    304   dynamic values for uniquely indexed columns, as we did for email
    305   above, made a test suite run between 2x to 3x faster.
    306 
    307   Deadlocks may happen in other circumstances. If you believe you
    308   are hitting a scenario that has not been described here, please
    309   report an issue so we can improve our examples. As a last resort,
    310   you can always disable the test triggering the deadlock from
    311   running asynchronously by setting  "async: false".
    312   """
    313 
    314   defmodule Connection do
    315     @moduledoc false
    316     if Code.ensure_loaded?(DBConnection) do
    317       @behaviour DBConnection
    318     end
    319 
    320     def connect(_opts) do
    321       raise "should never be invoked"
    322     end
    323 
    324     def disconnect(err, {conn_mod, state, _in_transaction?}) do
    325       conn_mod.disconnect(err, state)
    326     end
    327 
    328     def checkout(state), do: proxy(:checkout, state, [])
    329     def checkin(state), do: proxy(:checkin, state, [])
    330     def ping(state), do: proxy(:ping, state, [])
    331 
    332     def handle_begin(opts, {conn_mod, state, false}) do
    333       opts = [mode: :savepoint] ++ opts
    334 
    335       case conn_mod.handle_begin(opts, state) do
    336         {:ok, value, state} ->
    337           {:ok, value, {conn_mod, state, true}}
    338         {kind, err, state} ->
    339           {kind, err, {conn_mod, state, false}}
    340       end
    341     end
    342     def handle_commit(opts, {conn_mod, state, true}) do
    343       opts = [mode: :savepoint] ++ opts
    344       proxy(:handle_commit, {conn_mod, state, false}, [opts])
    345     end
    346     def handle_rollback(opts, {conn_mod, state, _}) do
    347       opts = [mode: :savepoint] ++ opts
    348       proxy(:handle_rollback, {conn_mod, state, false}, [opts])
    349     end
    350 
    351     def handle_status(opts, state),
    352       do: proxy(:handle_status, state, [maybe_savepoint(opts, state)])
    353     def handle_prepare(query, opts, state),
    354       do: proxy(:handle_prepare, state, [query, maybe_savepoint(opts, state)])
    355     def handle_execute(query, params, opts, state),
    356       do: proxy(:handle_execute, state, [query, params, maybe_savepoint(opts, state)])
    357     def handle_close(query, opts, state),
    358       do: proxy(:handle_close, state, [query, maybe_savepoint(opts, state)])
    359     def handle_declare(query, params, opts, state),
    360       do: proxy(:handle_declare, state, [query, params, maybe_savepoint(opts, state)])
    361     def handle_fetch(query, cursor, opts, state),
    362       do: proxy(:handle_fetch, state, [query, cursor, maybe_savepoint(opts, state)])
    363     def handle_deallocate(query, cursor, opts, state),
    364       do: proxy(:handle_deallocate, state, [query, cursor, maybe_savepoint(opts, state)])
    365 
    366     defp maybe_savepoint(opts, {_, _, in_transaction?}) do
    367       if not in_transaction? and Keyword.get(opts, :sandbox_subtransaction, true) do
    368         [mode: :savepoint] ++ opts
    369       else
    370         opts
    371       end
    372     end
    373 
    374     defp proxy(fun, {conn_mod, state, in_transaction?}, args) do
    375       result = apply(conn_mod, fun, args ++ [state])
    376       pos = :erlang.tuple_size(result)
    377       :erlang.setelement(pos, result, {conn_mod, :erlang.element(pos, result), in_transaction?})
    378     end
    379   end
    380 
    381   @doc """
    382   Starts a process that owns the connection and returns its pid.
    383 
    384   The owner process is not linked to the caller, it is your responsibility to
    385   ensure it will be stopped. In tests, this is done by terminating the pool
    386   in an `ExUnit.Callbacks.on_exit/2` callback:
    387 
    388       setup tags do
    389         pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
    390         on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
    391         :ok
    392       end
    393 
    394   ## Options
    395 
    396     * `:shared` - if `true`, the pool runs in the shared mode. Defaults to `false`
    397 
    398   The remaining options are passed to `checkout/2`.
    399   """
    400   @doc since: "3.4.4"
    401   def start_owner!(repo, opts \\ []) do
    402     parent = self()
    403 
    404     {:ok, pid} =
    405       Agent.start(fn ->
    406         {shared, opts} = Keyword.pop(opts, :shared, false)
    407         :ok = checkout(repo, opts)
    408 
    409         if shared do
    410           :ok = mode(repo, {:shared, self()})
    411         else
    412           :ok = allow(repo, self(), parent)
    413         end
    414       end)
    415 
    416     pid
    417   end
    418 
    419   @doc """
    420   Stops an owner process started by `start_owner!/2`.
    421   """
    422   @doc since: "3.4.4"
    423   @spec stop_owner(pid()) :: :ok
    424   def stop_owner(pid) do
    425     GenServer.stop(pid)
    426   end
    427 
    428   @doc """
    429   Sets the mode for the `repo` pool.
    430 
    431   The modes can be:
    432 
    433     * `:auto` - this is the default mode. When trying to use the repository,
    434       processes can automatically checkout a connection without calling
    435       `checkout/2` or `start_owner/2` before. This is the mode you will run
    436       on before your test suite starts
    437 
    438     * `:manual` - in this mode, the connection always has to be explicitly
    439       checked before used. Other processes are allowed to use the same
    440       connection if they are explicitly allowed via `allow/4`. You usually
    441       set the mode to manual at the end of your `test/test_helper.exs` file.
    442       This is also the mode you will run your async tests in
    443 
    444     * `{:shared, pid}` - after checking out a connection in manual mode,
    445       you can change the mode to `{:shared, pid}`, where pid is the process
    446       that owns the connection, most often `{:shared, self()}`. This makes it
    447       so all processes can use the same connection as the one owned by the
    448       current process. This is the mode you will run your sync tests in
    449 
    450   Whenever you change the mode to `:manual` or `:auto`, all existing
    451   connections are checked in. Therefore, it is recommend to set those
    452   modes before your test suite starts, as otherwise you will check in
    453   connections being used in any other test running concurrently.
    454   """
    455   def mode(repo, mode)
    456       when (is_atom(repo) or is_pid(repo)) and mode in [:auto, :manual]
    457       when (is_atom(repo) or is_pid(repo)) and elem(mode, 0) == :shared and is_pid(elem(mode, 1)) do
    458     %{pid: pool, opts: opts} = lookup_meta!(repo)
    459     DBConnection.Ownership.ownership_mode(pool, mode, opts)
    460   end
    461 
    462   @doc """
    463   Checks a connection out for the given `repo`.
    464 
    465   The process calling `checkout/2` will own the connection
    466   until it calls `checkin/2` or until it crashes in which case
    467   the connection will be automatically reclaimed by the pool.
    468 
    469   ## Options
    470 
    471     * `:sandbox` - when true the connection is wrapped in
    472       a transaction. Defaults to true.
    473 
    474     * `:isolation` - set the query to the given isolation level.
    475 
    476     * `:ownership_timeout` - limits how long the connection can be
    477       owned. Defaults to the value in your repo config in
    478       `config/config.exs` (or preferably in `config/test.exs`), or
    479       120000 ms if not set. The timeout exists for sanity checking
    480       purposes, to ensure there is no connection leakage, and can
    481       be bumped whenever necessary.
    482 
    483   """
    484   def checkout(repo, opts \\ []) when is_atom(repo) or is_pid(repo) do
    485     %{pid: pool, opts: pool_opts} = lookup_meta!(repo)
    486 
    487     pool_opts =
    488       if Keyword.get(opts, :sandbox, true) do
    489         [
    490           post_checkout: &post_checkout(&1, &2, opts),
    491           pre_checkin: &pre_checkin(&1, &2, &3, opts)
    492         ] ++ pool_opts
    493       else
    494         pool_opts
    495       end
    496 
    497     pool_opts_overrides = Keyword.take(opts, [:ownership_timeout, :isolation_level])
    498     pool_opts = Keyword.merge(pool_opts, pool_opts_overrides)
    499 
    500     case DBConnection.Ownership.ownership_checkout(pool, pool_opts) do
    501       :ok ->
    502         if isolation = opts[:isolation] do
    503           set_transaction_isolation_level(repo, isolation)
    504         end
    505 
    506         :ok
    507 
    508       other ->
    509         other
    510     end
    511   end
    512 
    513   defp set_transaction_isolation_level(repo, isolation) do
    514     query = "SET TRANSACTION ISOLATION LEVEL #{isolation}"
    515 
    516     case Ecto.Adapters.SQL.query(repo, query, [], sandbox_subtransaction: false) do
    517       {:ok, _} ->
    518         :ok
    519 
    520       {:error, error} ->
    521         checkin(repo, [])
    522         raise error
    523     end
    524   end
    525 
    526   @doc """
    527   Checks in the connection back into the sandbox pool.
    528   """
    529   def checkin(repo, _opts \\ []) when is_atom(repo) or is_pid(repo) do
    530     %{pid: pool, opts: opts} = lookup_meta!(repo)
    531     DBConnection.Ownership.ownership_checkin(pool, opts)
    532   end
    533 
    534   @doc """
    535   Allows the `allow` process to use the same connection as `parent`.
    536 
    537   `allow` may be a PID or a locally registered name.
    538   """
    539   def allow(repo, parent, allow, _opts \\ []) when is_atom(repo) or is_pid(repo) do
    540     case GenServer.whereis(allow) do
    541       pid when is_pid(pid) ->
    542         %{pid: pool, opts: opts} = lookup_meta!(repo)
    543         DBConnection.Ownership.ownership_allow(pool, parent, pid, opts)
    544 
    545       other ->
    546         raise """
    547         only PID or a locally registered process can be allowed to \
    548         use the same connection as parent but the lookup returned #{inspect(other)}
    549         """
    550     end
    551   end
    552 
    553   @doc """
    554   Runs a function outside of the sandbox.
    555   """
    556   def unboxed_run(repo, fun) when is_atom(repo) or is_pid(repo) do
    557     checkin(repo)
    558     checkout(repo, sandbox: false)
    559 
    560     try do
    561       fun.()
    562     after
    563       checkin(repo)
    564     end
    565   end
    566 
    567   defp lookup_meta!(repo) do
    568     %{opts: opts} =
    569       meta =
    570       repo
    571       |> find_repo()
    572       |> Ecto.Adapter.lookup_meta()
    573 
    574     if opts[:pool] != DBConnection.Ownership do
    575       raise """
    576       cannot invoke sandbox operation with pool #{inspect(opts[:pool])}.
    577       To use the SQL Sandbox, configure your repository pool as:
    578 
    579           pool: #{inspect(__MODULE__)}
    580       """
    581     end
    582 
    583     meta
    584   end
    585 
    586   defp find_repo(repo) when is_atom(repo), do: repo.get_dynamic_repo()
    587   defp find_repo(repo), do: repo
    588 
    589   defp post_checkout(conn_mod, conn_state, opts) do
    590     case conn_mod.handle_begin([mode: :transaction] ++ opts, conn_state) do
    591       {:ok, _, conn_state} ->
    592         {:ok, Connection, {conn_mod, conn_state, false}}
    593 
    594       {_error_or_disconnect, err, conn_state} ->
    595         {:disconnect, err, conn_mod, conn_state}
    596     end
    597   end
    598 
    599   defp pre_checkin(:checkin, Connection, {conn_mod, conn_state, _in_transaction?}, opts) do
    600     case conn_mod.handle_rollback([mode: :transaction] ++ opts, conn_state) do
    601       {:ok, _, conn_state} ->
    602         {:ok, conn_mod, conn_state}
    603 
    604       {:idle, _conn_state} ->
    605         raise """
    606         Ecto SQL sandbox transaction was already committed/rolled back.
    607 
    608         The sandbox works by running each test in a transaction and closing the\
    609         transaction afterwards. However, the transaction has already terminated.\
    610         Your test code is likely committing or rolling back transactions manually,\
    611         either by invoking procedures or running custom SQL commands.
    612 
    613         One option is to manually checkout a connection without a sandbox:
    614 
    615             Ecto.Adapters.SQL.Sandbox.checkout(repo, sandbox: false)
    616 
    617         But remember you will have to undo any database changes performed by such tests.
    618         """
    619 
    620       {_error_or_disconnect, err, conn_state} ->
    621         {:disconnect, err, conn_mod, conn_state}
    622     end
    623   end
    624 
    625   defp pre_checkin(_, Connection, {conn_mod, conn_state, _in_transaction?}, _opts) do
    626     {:ok, conn_mod, conn_state}
    627   end
    628 end