ownership.ex (5755B)
1 defmodule DBConnection.OwnershipError do 2 defexception [:message] 3 4 def exception(message), do: %DBConnection.OwnershipError{message: message} 5 end 6 7 defmodule DBConnection.Ownership do 8 @moduledoc """ 9 A DBConnection pool that requires explicit checkout and checkin 10 as a mechanism to coordinate between processes. 11 12 ## Options 13 14 * `:ownership_mode` - When mode is `:manual`, all connections must 15 be explicitly checked out before by using `ownership_checkout/2`. 16 Otherwise, mode is `:auto` and connections are checked out 17 implicitly. `{:shared, owner}` mode is also supported so 18 processes are allowed on demand. On all cases, checkins are 19 explicit via `ownership_checkin/2`. Defaults to `:auto`. 20 * `:ownership_timeout` - The maximum time that a process is allowed to own 21 a connection, default `120_000`. This timeout exists mostly for sanity 22 checking purposes and can be increased at will, since DBConnection 23 automatically checks in connections whenever there is a mode change. 24 * `:ownership_log` - The `Logger.level` to log ownership changes, or `nil` 25 not to log, default `nil`. 26 27 There are also two experimental options, `:post_checkout` and `:pre_checkin` 28 which allows a developer to configure what happens when a connection is 29 checked out and checked in. Those options are meant to be used during tests, 30 and have the following behaviour: 31 32 * `:post_checkout` - it must be an anonymous function that receives the 33 connection module, the connection state and it must return either 34 `{:ok, connection_module, connection_state}` or 35 `{:disconnect, err, connection_module, connection_state}`. This allows 36 the developer to change the connection module on post checkout. However, 37 in case of disconnects, the return `connection_module` must be the same 38 as the `connection_module` given. Defaults to simply returning the given 39 connection module and state. 40 41 * `:pre_checkin` - it must be an anonymous function that receives the 42 checkin reason (`:checkin`, `{:disconnect, err}` or `{:stop, err}`), 43 the connection module and the connection state returned by `post_checkout`. 44 It must return either `{:ok, connection_module, connection_state}` or 45 `{:disconnect, err, connection_module, connection_state}` where the connection 46 module is the module given to `:post_checkout` Defaults to simply returning 47 the given connection module and state. 48 49 ## Callers lookup 50 51 When checking out, the ownership pool first looks if there is a connection 52 assigned to the current process and then checks if there is a connection 53 assigned to any of the processes listed under the `$callers` process 54 dictionary entry. The `$callers` entry is set by default for tasks from 55 Elixir v1.8. 56 57 You can also pass the `:caller` option on checkout with a pid and that 58 pid will be looked up first, instead of `self()`, and then we fall back 59 to `$callers`. 60 """ 61 62 alias DBConnection.Ownership.Manager 63 alias DBConnection.Holder 64 65 @doc false 66 defdelegate child_spec(args), to: Manager 67 68 @doc false 69 defdelegate disconnect_all(pool, interval, opts), to: Manager 70 71 @doc false 72 def checkout(pool, callers, opts) do 73 case Manager.proxy_for(callers, opts) do 74 {caller, pool} -> Holder.checkout(pool, [caller], opts) 75 nil -> Holder.checkout(pool, callers, opts) 76 end 77 end 78 79 @doc """ 80 Explicitly checks a connection out from the ownership manager. 81 82 It may return `:ok` if the connection is checked out. 83 `{:already, :owner | :allowed}` if the caller process already 84 has a connection, `:error` if it could be not checked out or 85 raise if there was an error. 86 """ 87 @spec ownership_checkout(GenServer.server(), Keyword.t()) :: 88 :ok | {:already, :owner | :allowed} 89 def ownership_checkout(manager, opts) do 90 with {:ok, pid} <- Manager.checkout(manager, opts) do 91 case Holder.checkout(pid, [self()], opts) do 92 {:ok, pool_ref, _module, _idle_time, _state} -> 93 Holder.checkin(pool_ref) 94 95 {:error, err} -> 96 raise err 97 end 98 end 99 end 100 101 @doc """ 102 Changes the ownership mode. 103 104 `mode` may be `:auto`, `:manual` or `{:shared, owner}`. 105 106 The operation will always succeed when setting the mode to 107 `:auto` or `:manual`. It may fail with reason `:not_owner` 108 or `:not_found` when setting `{:shared, pid}` and the 109 given pid does not own any connection. May return 110 `:already_shared` if another process set the ownership 111 mode to `{:shared, _}` and is still alive. 112 """ 113 @spec ownership_mode(GenServer.server(), :auto | :manual | {:shared, pid}, Keyword.t()) :: 114 :ok | :already_shared | :not_owner | :not_found 115 defdelegate ownership_mode(manager, mode, opts), to: Manager, as: :mode 116 117 @doc """ 118 Checks a connection back in. 119 120 A connection can only be checked back in by its owner. 121 """ 122 @spec ownership_checkin(GenServer.server(), Keyword.t()) :: 123 :ok | :not_owner | :not_found 124 defdelegate ownership_checkin(manager, opts), to: Manager, as: :checkin 125 126 @doc """ 127 Allows the process given by `allow` to use the connection checked out 128 by `owner_or_allowed`. 129 130 It may return `:ok` if the connection is checked out. 131 `{:already, :owner | :allowed}` if the `allow` process already 132 has a connection. `owner_or_allowed` may either be the owner or any 133 other allowed process. Returns `:not_found` if the given process 134 does not have any connection checked out. 135 """ 136 @spec ownership_allow(GenServer.server(), owner_or_allowed :: pid, allow :: pid, Keyword.t()) :: 137 :ok | {:already, :owner | :allowed} | :not_found 138 defdelegate ownership_allow(manager, owner, allow, opts), to: Manager, as: :allow 139 end