View Source NimbleOwnership (NimbleOwnership v0.2.0)
Module that allows you to manage ownership of resources across processes.
The idea is that you can track ownership of terms (keys) across processes, and allow processes to use a key through processes that are already allowed.
flowchart LR
pidA["Process A"]
pidB["Process B"]
pidC["Process C"]
res(["Resource (with associated metadata)"])
pidA -->|Owns| res
pidA -->|Allows| pidB
pidB -->|Can access| res
pidB -->|Allows| pidC
pidC -->|Can access| res
A typical use case for such a module is tracking resource ownership across processes in order to isolate access to resources in test suites. For example, the Mox library uses this module to track ownership of mocks across processes (in shared mode).
Usage
To track ownership of resources, you need to start a NimbleOwnership
server (a process),
through start_link/1
or child_spec/1
.
Then, you can allow a process access to a key through allow/4
. You can then check
if a PID can access the given key through fetch_owner/3
.
Metadata
You can store arbitrary metadata (metadata/0
) alongside each owned resource.
This metadata is returned together with the owner PID when you call fetch_owner/3
.
Modes
The ownership server can be in one of two modes:
private (the default): in this mode, you can only allow access to a key through the owner PID or PIDs that are already allowed to access the key. You can allow PIDs through
allow/4
. This mode is useful when you want to track ownership of resources in concurrent environments (such as in a test suite).shared: in this mode, there is only one shared owner PID that owns all the keys in the ownership server. Any other PID can read the metadata associated with any key, but it cannot update the metadata (only the shared owner can).
Returning to Private Mode
If the ownership server is in shared mode and the shared owner process terminates, the server automatically returns to private mode.
Summary
Functions
Allows pid_to_allow
to use key
through owner_pid
(on the given ownership_server
).
Returns a specification to start this module under a supervisor.
Gets the owner of key
through one of the callers
.
Accesses key
(owned by owner_pid
) or initializes the ownership.
Gets all the keys owned by owner_pid
with all their associated metadata.
Sets the ownership server to private mode.
Sets the ownership server to shared mode and sets shared_owner
as the shared owner.
Starts an ownership server.
Types
@type key() :: term()
Arbitrary key.
@type metadata() :: term()
Arbitrary metadata associated with an owned key/0
.
@type server() :: GenServer.server()
Ownership server.
Functions
allow(ownership_server, owner_pid, pid_to_allow, key, timeout \\ 5000)
View SourceAllows pid_to_allow
to use key
through owner_pid
(on the given ownership_server
).
Use this function when owner_pid
is allowed access to key
, and you want
to also allow pid_to_allow
to use key
.
This function return an error in the following cases:
When
pid_to_allow
is already allowed to usekey
via another owner PID that is notowner_pid
. In this case, the:reason
field of the returnedNimbleOwnership.Error
struct is set to{:already_allowed, other_owner_pid}
.When the ownership server is in shared mode. In this case, the
:reason
field of the returnedNimbleOwnership.Error
struct is set to:cant_allow_in_shared_mode
.
Tracking Callers
The ownership server automatically considers all the direct and indirect "children" of an owner PID as allowed to access the owner's keys. By children, we mean processes that have been spawned by the owner PID or by any of its children, which is something we determine by looking at the
:"$callers"
key in the process dictionary. This is useful for many standard process kinds. For example, if a process owns a key and starts a task withTask.start_link/1
, then the task will be allowed to access the key without having to explicitly callallow/4
.
Examples
iex> pid = spawn(fn -> Process.sleep(:infinity) end)
iex> {:ok, server} = NimbleOwnership.start_link()
iex> NimbleOwnership.get_and_update(server, self(), :my_key, fn _ -> {:updated, _meta = %{}} end)
{:ok, :updated}
iex> NimbleOwnership.allow(server, self(), pid, :my_key)
:ok
iex> NimbleOwnership.fetch_owner(server, [pid], :my_key)
{:ok, self()}
Returns a specification to start this module under a supervisor.
See Supervisor
.
@spec fetch_owner(server(), [pid(), ...], key(), timeout()) :: {:ok, owner :: pid()} | {:shared_owner, shared_owner :: pid()} | {:error, reason} when reason: NimbleOwnership.Error.t()
Gets the owner of key
through one of the callers
.
If one of the callers
owns key
or is allowed access to key
,
then this function returns {:ok, {owner_pid, metadata}}
where metadata
is the
metadata associated with the key
under the owner.
If the ownership server is in shared mode, then this function
returns {:shared_owner, shared_owner_pid}
where shared_owner_pid
is the PID of the
shared owner. This is regardless of the callers
.
If none of the callers owns key
or is allowed access to key
, then this function
returns {:error, reason}
.
Examples
iex> pid = spawn(fn -> Process.sleep(:infinity) end)
iex> {:ok, server} = NimbleOwnership.start_link()
iex> NimbleOwnership.set_mode_to_shared(server, pid)
iex> {:shared_owner, owner_pid} = NimbleOwnership.fetch_owner(server, [self()], :whatever_key)
iex> pid == owner_pid
true
get_and_update(ownership_server, owner_pid, key, fun, timeout \\ 5000)
View Source@spec get_and_update(server(), pid(), key(), fun, timeout()) :: {:ok, get_value} | {:error, NimbleOwnership.Error.t()} when fun: (nil | metadata() -> {get_value, updated_metadata :: metadata()}), get_value: term()
Accesses key
(owned by owner_pid
) or initializes the ownership.
Use this function for these purposes:
- to initialize the ownership of a key
- to update the metadata associated with a key
Usage
When owner_pid
doesn't own key
, the value passed to fun
will be nil
. Otherwise,
it will be the current metadata associated with key
under the owner owner_pid
.
fun
must return {get_value, new_meta}
. owner_pid
will start owning
key
and new_meta
will be the metadata associated with that ownership, or,
in case owner_pid
already owned key
, then the metadata is updated to new_meta
.
If this function is successful, the return value is {:ok, get_value}
where get_value
is the value returned by fun
in its return tuple. Otherwise, the return value is
{:error, reason}
(see also NimbleOwnership.Error
).
Updating Metadata from an Allowed Process
If you don't directly have access to the owner PID, but you want to update the metadata
associated with the owner PID and key
from an allowed process, do this instead:
- Fetch the owner of
key
throughfetch_owner/3
. - Call
get_and_update/4
with the owner PID asowner_pid
, passing in a callback function that returns the new metadata.
Shared Mode
When the ownership server is set to shared mode, you can only call this function
with owner_pid
set to the shared owner PID. See the module documentation.
get_owned(ownership_server, owner_pid, default \\ nil, timeout \\ 5000)
View Source@spec get_owned(server(), pid(), default, timeout()) :: %{required(key()) => metadata()} | default when default: term()
Gets all the keys owned by owner_pid
with all their associated metadata.
If owner_pid
doesn't own any keys, then this function returns default
.
Examples
iex> owner = spawn(fn -> Process.sleep(:infinity) end)
iex> {:ok, server} = NimbleOwnership.start_link()
iex> NimbleOwnership.get_and_update(server, owner, :my_key1, fn _ -> {:ok, 1} end)
iex> NimbleOwnership.get_and_update(server, owner, :my_key2, fn _ -> {:ok, 2} end)
iex> NimbleOwnership.get_owned(server, owner)
%{my_key1: 1, my_key2: 2}
iex> NimbleOwnership.get_owned(server, self(), :default)
:default
@spec set_mode_to_private(server()) :: :ok
Sets the ownership server to private mode.
See the module documentation for more information.
@spec start_link(keyword()) :: GenServer.on_start()
Starts an ownership server.
Options
This function supports all the options supported by GenServer.start_link/3
, namely:
:name
:timeout
:debug
:spawn_opt
:hibernate_after