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