Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions lib/cachex/services/overseer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,21 @@ defmodule Cachex.Services.Overseer do

Retrieving a cache will map the provided argument to a
cache record if available, otherwise a nil value.

When a name resolver is configured (see `resolve_name/1`), the name is
passed through it first, allowing the caller to be transparently routed
to a different cache instance. This makes per-process redirection (e.g.
test sandboxing) possible without intercepting every cache call.
"""
@spec lookup(Cachex.t()) :: Cachex.t() | nil
def lookup(cache() = cache),
do: cache

def lookup(name) when is_atom(name) do
case :ets.lookup(@table_name, name) do
[{^name, state}] ->
resolved = resolve_name(name)

case :ets.lookup(@table_name, resolved) do
[{^resolved, state}] ->
state

_other ->
Expand All @@ -80,6 +87,34 @@ defmodule Cachex.Services.Overseer do
def lookup(_any),
do: nil

@doc """
Resolves a cache name through the optionally-configured resolver.

By default this is the identity function: the name is returned
unchanged and there is no measurable overhead. Setting

config :cachex, :name_resolver, &MyModule.resolve/1

installs a `(atom -> atom)` function that is consulted on every cache
name resolution. It must return a cache name (an atom); returning the
same name is a no-op. This is the supported extension point for
redirecting cache resolution per process — for example, a test-isolation
library can return a per-test cache name based on the calling process
(via the process dictionary or `$callers`), giving each async test its
own isolated cache without forking or patching Cachex.

Resolution is **not** applied recursively: the resolver's result is used
directly as the ETS key, so a resolver must return a concrete name, not
another name that itself needs resolving.
"""
@spec resolve_name(atom) :: atom
def resolve_name(name) when is_atom(name) do
case Application.get_env(:cachex, :name_resolver) do
nil -> name
resolver when is_function(resolver, 1) -> resolver.(name) || name
end
end

@doc """
Registers a cache record against a name.
"""
Expand Down
42 changes: 42 additions & 0 deletions test/cachex/services/overseer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,46 @@ defmodule Cachex.OverseerTest do
# now we need to make sure our state was forwarded
assert_receive({:cache, ^update2})
end

# With no resolver configured, resolve_name/1 is the identity function
# and lookup/1 behaves exactly as before.
test "resolve_name/1 returns the name unchanged by default" do
assert(Services.Overseer.resolve_name(:some_cache) == :some_cache)
end

# A configured resolver redirects name resolution, so lookup/1 returns
# the state registered under the resolved name. This is the supported
# hook for per-process redirection (e.g. test sandboxing).
test "lookup/1 routes through a configured name resolver" do
real = TestUtils.create_name()
alias_name = TestUtils.create_name()

state = cache(name: real)
Services.Overseer.register(real, state)

# Resolver maps the alias to the real registered name.
Application.put_env(:cachex, :name_resolver, fn
^alias_name -> real
other -> other
end)

on_exit(fn -> Application.delete_env(:cachex, :name_resolver) end)

# Looking up the alias resolves to the real cache's state...
assert(Services.Overseer.lookup(alias_name) == state)
# ...while the real name still resolves to itself.
assert(Services.Overseer.lookup(real) == state)
end

# A resolver returning nil falls back to the original name (no redirect).
test "a resolver returning nil falls back to the original name" do
name = TestUtils.create_name()
state = cache(name: name)
Services.Overseer.register(name, state)

Application.put_env(:cachex, :name_resolver, fn _ -> nil end)
on_exit(fn -> Application.delete_env(:cachex, :name_resolver) end)

assert(Services.Overseer.lookup(name) == state)
end
end