diff --git a/lib/cache.ex b/lib/cache.ex index 9c119f2..4c8de4f 100644 --- a/lib/cache.ex +++ b/lib/cache.ex @@ -400,17 +400,11 @@ defmodule Cache do Cache.get_or_create(__MODULE__, key, fnc) end - if @cache_opts[:sandbox?] do - defp maybe_sandbox_key(key) do - sandbox_id = Cache.SandboxRegistry.find!(__MODULE__) - - "#{sandbox_id}:#{key}" - end - else - defp maybe_sandbox_key(key) do - key - end - end + # In sandbox mode, Cache.Sandbox handles per-caller isolation internally + # by scoping state under the current SandboxRegistry sandbox_id. Keys + # are not rewritten here, so scan/match operations see only the current + # sandbox's data without needing a prefix filter. + defp maybe_sandbox_key(key), do: key end end end diff --git a/lib/cache/sandbox.ex b/lib/cache/sandbox.ex index d2c0f7a..d56442f 100644 --- a/lib/cache/sandbox.ex +++ b/lib/cache/sandbox.ex @@ -32,6 +32,14 @@ defmodule Cache.Sandbox do proper isolation between test cases. > **Note**: This adapter should not be used in production environments. + + ## Isolation + + Each Agent process keeps state shaped as `%{sandbox_id => %{key => value}}`. The + `sandbox_id` is resolved per-caller via `Cache.SandboxRegistry` BEFORE entering + the Agent callback (so the caller's `self()`/`$callers`/`$ancestors` are used + for lookup, not the Agent's). When no sandbox is registered for the caller we + fall back to `:__unscoped__` — keeping the adapter usable outside of test. """ use Agent @@ -41,6 +49,9 @@ defmodule Cache.Sandbox do @behaviour Cache + @sandbox_registry :elixir_cache_sandbox + @unscoped :__unscoped__ + @impl Cache def opts_definition, do: [] @@ -59,85 +70,123 @@ defmodule Cache.Sandbox do } end + # SECTION: sandbox scoping helpers + + # Resolve the sandbox id for the current caller. Must be called from the + # caller process (NOT inside the Agent callback) so SandboxRegistry can walk + # the caller's $callers/$ancestors. + defp current_sandbox_id(cache_name) do + case Process.whereis(@sandbox_registry) do + nil -> + @unscoped + + _pid -> + case SandboxRegistry.lookup(@sandbox_registry, cache_name) do + {:ok, id} -> id + {:error, _} -> @unscoped + end + end + end + + defp scoped_get(state, sid), do: Map.get(state, sid, %{}) + defp scoped_put(state, sid, sub), do: Map.put(state, sid, sub) + + defp scoped_agent_get(cache_name, fun) do + sid = current_sandbox_id(cache_name) + Agent.get(cache_name, fn state -> fun.(scoped_get(state, sid)) end) + end + + defp scoped_agent_update(cache_name, fun) do + sid = current_sandbox_id(cache_name) + Agent.update(cache_name, fn state -> scoped_put(state, sid, fun.(scoped_get(state, sid))) end) + end + + defp scoped_agent_get_and_update(cache_name, fun) do + sid = current_sandbox_id(cache_name) + + Agent.get_and_update(cache_name, fn state -> + {reply, new_sub} = fun.(scoped_get(state, sid)) + {reply, scoped_put(state, sid, new_sub)} + end) + end + + # SECTION: core cache API + @impl Cache def get(cache_name, key, _opts \\ []) do - Agent.get(cache_name, fn state -> - {:ok, Map.get(state, key)} - end) + scoped_agent_get(cache_name, fn sub -> {:ok, Map.get(sub, key)} end) end @impl Cache def put(cache_name, key, _ttl \\ nil, value, _opts \\ []) do - Agent.update(cache_name, fn state -> - Map.put(state, key, value) - end) + scoped_agent_update(cache_name, fn sub -> Map.put(sub, key, value) end) end @impl Cache def delete(cache_name, key, _opts \\ []) do - Agent.update(cache_name, fn state -> - Map.delete(state, key) - end) + scoped_agent_update(cache_name, fn sub -> Map.delete(sub, key) end) end def get_or_store(cache_name, key, _ttl, store_fun) do - Agent.get_and_update(cache_name, fn state -> - case Map.fetch(state, key) do + scoped_agent_get_and_update(cache_name, fn sub -> + case Map.fetch(sub, key) do {:ok, value} -> - {value, state} + {value, sub} :error -> value = store_fun.() - {value, Map.put(state, key, value)} + {value, Map.put(sub, key, value)} end end) end def dirty_get_or_store(cache_name, key, store_fun) do - Agent.get_and_update(cache_name, fn state -> - case Map.fetch(state, key) do + scoped_agent_get_and_update(cache_name, fn sub -> + case Map.fetch(sub, key) do {:ok, value} -> - {value, state} + {value, sub} :error -> value = store_fun.() - {value, Map.put(state, key, value)} + {value, Map.put(sub, key, value)} end end) end + # SECTION: hash API + def hash_delete(cache_name, key, hash_key, _opts) do - Agent.get_and_update(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get_and_update(cache_name, fn sub -> + case Map.get(sub, key) do nil -> - {{:ok, 0}, state} + {{:ok, 0}, sub} value when is_map(value) -> if Map.has_key?(value, hash_key) do updated = Map.delete(value, hash_key) - new_state = + new_sub = if Enum.empty?(updated) do - Map.delete(state, key) + Map.delete(sub, key) else - Map.put(state, key, updated) + Map.put(sub, key, updated) end - {{:ok, 1}, new_state} + {{:ok, 1}, new_sub} else - {{:ok, 0}, state} + {{:ok, 0}, sub} end _ -> - {{:ok, 0}, state} + {{:ok, 0}, sub} end end) end def hash_get(cache_name, key, hash_key, _opts) do - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> value = - state + sub |> Map.get(key, %{}) |> Map.get(hash_key) @@ -146,8 +195,8 @@ defmodule Cache.Sandbox do end def hash_get_all(cache_name, key, _opts) do - Agent.get(cache_name, fn state -> - case state[key] do + scoped_agent_get(cache_name, fn sub -> + case sub[key] do nil -> {:ok, %{}} value -> {:ok, value} end @@ -155,10 +204,10 @@ defmodule Cache.Sandbox do end def hash_get_many(cache_name, keys_fields, _opts) do - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> values = Enum.map(keys_fields, fn {key, fields} -> - hash = Map.get(state, key, %{}) + hash = Map.get(sub, key, %{}) Enum.map(fields, &Map.get(hash, &1)) end) @@ -167,19 +216,19 @@ defmodule Cache.Sandbox do end def hash_values(cache_name, key, _opts) do - Agent.get(cache_name, fn state -> - {:ok, Map.values(state[key] || %{})} + scoped_agent_get(cache_name, fn sub -> + {:ok, Map.values(sub[key] || %{})} end) end def hash_set(cache_name, key, field, value, ttl, _opts) do count = - Agent.get_and_update(cache_name, fn state -> - hash = Map.get(state, key, %{}) + scoped_agent_get_and_update(cache_name, fn sub -> + hash = Map.get(sub, key, %{}) is_new_field = not Map.has_key?(hash, field) updated_hash = Map.put(hash, field, value) - {if(is_new_field, do: 1, else: 0), Map.put(state, key, updated_hash)} + {if(is_new_field, do: 1, else: 0), Map.put(sub, key, updated_hash)} end) if ttl do @@ -191,9 +240,9 @@ defmodule Cache.Sandbox do def hash_set_many(cache_name, keys_fields_values, ttl, _opts) do counts = - Agent.get_and_update(cache_name, fn state -> - {counts, new_state} = - Enum.map_reduce(keys_fields_values, state, fn {key, fields_values}, acc -> + scoped_agent_get_and_update(cache_name, fn sub -> + {counts, new_sub} = + Enum.map_reduce(keys_fields_values, sub, fn {key, fields_values}, acc -> hash = Map.get(acc, key, %{}) {updated_hash, count} = @@ -208,7 +257,7 @@ defmodule Cache.Sandbox do {count, Map.put(acc, key, updated_hash)} end) - {counts, new_state} + {counts, new_sub} end) if ttl do @@ -219,6 +268,8 @@ defmodule Cache.Sandbox do end end + # SECTION: JSON API + def json_get(cache_name, key, path, _opts) when path in [nil, ["."]] do get(cache_name, key) end @@ -240,16 +291,16 @@ defmodule Cache.Sandbox do end def json_set(cache_name, key, path, value, _opts) do - state = Agent.get(cache_name, & &1) + sub = scoped_agent_get(cache_name, & &1) path = JSON.serialize_path(path) - with :ok <- check_key_exists(state, key), - :ok <- check_path_exists(state, key, path) do + with :ok <- check_key_exists(sub, key), + :ok <- check_path_exists(sub, key, path) do path = add_defaults([key | String.split(path, ".")]) value = stringify_value(value) - Agent.update(cache_name, fn state -> - put_in(state, path, value) + scoped_agent_update(cache_name, fn sub -> + put_in(sub, path, value) end) end end @@ -258,14 +309,14 @@ defmodule Cache.Sandbox do path_parts = json_path_parts(path) path_string = json_path_string(path) - Agent.get_and_update(cache_name, fn state -> - case get_in(state, [key | path_parts]) do + scoped_agent_get_and_update(cache_name, fn sub -> + case get_in(sub, [key | path_parts]) do nil -> - {{:error, ErrorMessage.not_found("ERR Path '$.#{path_string}' does not exist")}, state} + {{:error, ErrorMessage.not_found("ERR Path '$.#{path_string}' does not exist")}, sub} value -> new_value = value + incr - {{:ok, new_value}, put_in(state, [key | path_parts], new_value)} + {{:ok, new_value}, put_in(sub, [key | path_parts], new_value)} end end) end @@ -273,10 +324,10 @@ defmodule Cache.Sandbox do def json_clear(cache_name, key, path, _opts) do path_parts = json_path_parts(path) - Agent.get_and_update(cache_name, fn state -> - case get_in(state, [key | path_parts]) do + scoped_agent_get_and_update(cache_name, fn sub -> + case get_in(sub, [key | path_parts]) do nil -> - {{:ok, 0}, state} + {{:ok, 0}, sub} value -> updated_value = @@ -287,7 +338,7 @@ defmodule Cache.Sandbox do _ -> nil end - {{:ok, 1}, put_in(state, [key | path_parts], updated_value)} + {{:ok, 1}, put_in(sub, [key | path_parts], updated_value)} end end) end @@ -295,13 +346,13 @@ defmodule Cache.Sandbox do def json_delete(cache_name, key, path, _opts) do path_parts = json_path_parts(path) - Agent.get_and_update(cache_name, fn state -> - case get_in(state, [key | path_parts]) do + scoped_agent_get_and_update(cache_name, fn sub -> + case get_in(sub, [key | path_parts]) do nil -> - {{:ok, 0}, state} + {{:ok, 0}, sub} _value -> - {_, updated} = pop_in(state, [key | path_parts]) + {_, updated} = pop_in(sub, [key | path_parts]) {{:ok, 1}, updated} end end) @@ -320,23 +371,24 @@ defmodule Cache.Sandbox do path_string = json_path_string(path) updated_values = Enum.map(values, value_transformer) - Agent.get_and_update(cache_name, fn state -> - case get_in(state, [key | path_parts]) do + scoped_agent_get_and_update(cache_name, fn sub -> + case get_in(sub, [key | path_parts]) do nil -> - {{:error, ErrorMessage.not_found("ERR Path '$.#{path_string}' does not exist")}, state} + {{:error, ErrorMessage.not_found("ERR Path '$.#{path_string}' does not exist")}, sub} list when is_list(list) -> updated_list = list ++ updated_values - new_state = put_in(state, [key | path_parts], updated_list) - {{:ok, enum_length(updated_list)}, new_state} + new_sub = put_in(sub, [key | path_parts], updated_list) + {{:ok, enum_length(updated_list)}, new_sub} _ -> - {{:error, ErrorMessage.not_found("ERR Path '$.#{path_string}' does not exist")}, state} + {{:error, ErrorMessage.not_found("ERR Path '$.#{path_string}' does not exist")}, sub} end end) end - # Redis Compatibility + # SECTION: Redis compatibility + def pipeline(_cache_name, _commands, _opts) do raise "Not Implemented" end @@ -358,13 +410,13 @@ defmodule Cache.Sandbox do count = scan_opts[:count] type = scan_opts[:type] - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> values = - state + sub |> Stream.filter(fn {_key, value} -> scan_type_match?(value, type) end) - |> Stream.map(fn {key, _value} -> {key, scan_key(key)} end) - |> Stream.filter(fn {_key, match_key} -> scan_match?(match_key, match) end) - |> Enum.map(fn {_key, match_key} -> match_key end) + |> Stream.map(fn {key, _value} -> key end) + |> Stream.filter(fn key -> scan_match?(key, match) end) + |> Enum.to_list() |> apply_scan_count(count) {:ok, values} @@ -375,8 +427,8 @@ defmodule Cache.Sandbox do match = scan_opts[:match] || "*" count = scan_opts[:count] - Agent.get(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get(cache_name, fn sub -> + case Map.get(sub, key) do map when is_map(map) -> elements = map @@ -391,31 +443,36 @@ defmodule Cache.Sandbox do end) end - # ETS & DETS Compatibility + # SECTION: ETS & DETS compatibility + # + # All ETS/DETS-style helpers below operate on the scoped sub-map for the + # current sandbox so tests stay isolated from each other. def all do - Agent.get(Cache.Sandbox, fn state -> Map.keys(state) end) + sid = current_sandbox_id(Cache.Sandbox) + + Agent.get(Cache.Sandbox, fn state -> state |> scoped_get(sid) |> Map.keys() end) rescue _ -> [] end def delete_table(cache_name) do - Agent.update(cache_name, fn _ -> %{} end) + scoped_agent_update(cache_name, fn _ -> %{} end) true end def delete_all_objects(cache_name) do - Agent.update(cache_name, fn _ -> %{} end) + scoped_agent_update(cache_name, fn _ -> %{} end) true end def delete_object(cache_name, object) when is_tuple(object) do key = elem(object, 0) - Agent.update(cache_name, fn state -> - case Map.get(state, key) do - ^object -> Map.delete(state, key) - _ -> state + scoped_agent_update(cache_name, fn sub -> + case Map.get(sub, key) do + ^object -> Map.delete(sub, key) + _ -> sub end end) @@ -423,8 +480,8 @@ defmodule Cache.Sandbox do end def first(cache_name) do - Agent.get(cache_name, fn state -> - case Map.keys(state) do + scoped_agent_get(cache_name, fn sub -> + case Map.keys(sub) do [] -> :"$end_of_table" [key | _] -> key end @@ -432,8 +489,8 @@ defmodule Cache.Sandbox do end def first_lookup(cache_name) do - Agent.get(cache_name, fn state -> - case Map.to_list(state) do + scoped_agent_get(cache_name, fn sub -> + case Map.to_list(sub) do [] -> :"$end_of_table" [{key, value} | _] -> {key, [{key, value}]} end @@ -441,8 +498,8 @@ defmodule Cache.Sandbox do end def last(cache_name) do - Agent.get(cache_name, fn state -> - case state |> Map.keys() |> Enum.reverse() do + scoped_agent_get(cache_name, fn sub -> + case sub |> Map.keys() |> Enum.reverse() do [] -> :"$end_of_table" [key | _] -> key end @@ -450,8 +507,8 @@ defmodule Cache.Sandbox do end def last_lookup(cache_name) do - Agent.get(cache_name, fn state -> - case state |> Map.to_list() |> Enum.reverse() do + scoped_agent_get(cache_name, fn sub -> + case sub |> Map.to_list() |> Enum.reverse() do [] -> :"$end_of_table" [{key, value} | _] -> {key, [{key, value}]} end @@ -459,37 +516,37 @@ defmodule Cache.Sandbox do end def next(cache_name, key) do - Agent.get(cache_name, fn state -> - keys = state |> Map.keys() |> Enum.sort() + scoped_agent_get(cache_name, fn sub -> + keys = sub |> Map.keys() |> Enum.sort() find_next_key(keys, key) end) end def next_lookup(cache_name, key) do - Agent.get(cache_name, fn state -> - keys = state |> Map.keys() |> Enum.sort() + scoped_agent_get(cache_name, fn sub -> + keys = sub |> Map.keys() |> Enum.sort() case find_next_key(keys, key) do :"$end_of_table" -> :"$end_of_table" - next_key -> {next_key, [{next_key, Map.get(state, next_key)}]} + next_key -> {next_key, [{next_key, Map.get(sub, next_key)}]} end end) end def prev(cache_name, key) do - Agent.get(cache_name, fn state -> - keys = state |> Map.keys() |> Enum.sort() |> Enum.reverse() + scoped_agent_get(cache_name, fn sub -> + keys = sub |> Map.keys() |> Enum.sort() |> Enum.reverse() find_next_key(keys, key) end) end def prev_lookup(cache_name, key) do - Agent.get(cache_name, fn state -> - keys = state |> Map.keys() |> Enum.sort() |> Enum.reverse() + scoped_agent_get(cache_name, fn sub -> + keys = sub |> Map.keys() |> Enum.sort() |> Enum.reverse() case find_next_key(keys, key) do :"$end_of_table" -> :"$end_of_table" - prev_key -> {prev_key, [{prev_key, Map.get(state, prev_key)}]} + prev_key -> {prev_key, [{prev_key, Map.get(sub, prev_key)}]} end end) end @@ -504,8 +561,8 @@ defmodule Cache.Sandbox do defp find_next_key([_ | rest], key), do: find_next_key(rest, key) def foldl(cache_name, function, acc) do - Agent.get(cache_name, fn state -> - state + scoped_agent_get(cache_name, fn sub -> + sub |> Map.to_list() |> Enum.reduce(acc, fn {key, value}, acc_inner -> function.({key, value}, acc_inner) @@ -514,8 +571,8 @@ defmodule Cache.Sandbox do end def foldr(cache_name, function, acc) do - Agent.get(cache_name, fn state -> - state + scoped_agent_get(cache_name, fn sub -> + sub |> Map.to_list() |> Enum.reverse() |> Enum.reduce(acc, fn {key, value}, acc_inner -> @@ -525,14 +582,12 @@ defmodule Cache.Sandbox do end def member(cache_name, key) do - Agent.get(cache_name, fn state -> - Map.has_key?(state, key) - end) + scoped_agent_get(cache_name, fn sub -> Map.has_key?(sub, key) end) end def lookup(cache_name, key) do - Agent.get(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get(cache_name, fn sub -> + case Map.get(sub, key) do nil -> [] value -> [{key, value}] end @@ -540,8 +595,8 @@ defmodule Cache.Sandbox do end def lookup_element(cache_name, key, pos) do - Agent.get(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get(cache_name, fn sub -> + case Map.get(sub, key) do nil -> raise ArgumentError, "key not found" value when is_tuple(value) -> elem(value, pos - 1) value -> value @@ -550,8 +605,8 @@ defmodule Cache.Sandbox do end def lookup_element(cache_name, key, pos, default) do - Agent.get(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get(cache_name, fn sub -> + case Map.get(sub, key) do nil -> default value when is_tuple(value) -> elem(value, pos - 1) value -> value @@ -560,33 +615,33 @@ defmodule Cache.Sandbox do end def update_counter(cache_name, key, {_pos, incr}) do - Agent.get_and_update(cache_name, fn state -> - current_value = state[key] || 0 + scoped_agent_get_and_update(cache_name, fn sub -> + current_value = sub[key] || 0 new_value = current_value + incr - {new_value, Map.put(state, key, new_value)} + {new_value, Map.put(sub, key, new_value)} end) end def update_counter(cache_name, key, incr) when is_integer(incr) do - Agent.get_and_update(cache_name, fn state -> - current_value = state[key] || 0 + scoped_agent_get_and_update(cache_name, fn sub -> + current_value = sub[key] || 0 new_value = current_value + incr - {new_value, Map.put(state, key, new_value)} + {new_value, Map.put(sub, key, new_value)} end) end def update_counter(cache_name, key, update_op, default) do - Agent.get_and_update(cache_name, fn state -> - if Map.has_key?(state, key) do - current_value = state[key] + scoped_agent_get_and_update(cache_name, fn sub -> + if Map.has_key?(sub, key) do + current_value = sub[key] incr = if is_tuple(update_op), do: elem(update_op, 1), else: update_op new_value = current_value + incr - {new_value, Map.put(state, key, new_value)} + {new_value, Map.put(sub, key, new_value)} else default_value = if is_tuple(default), do: elem(default, 1), else: default incr = if is_tuple(update_op), do: elem(update_op, 1), else: update_op new_value = default_value + incr - {new_value, Map.put(state, key, new_value)} + {new_value, Map.put(sub, key, new_value)} end end) end @@ -595,16 +650,13 @@ defmodule Cache.Sandbox do key = elem(data, 0) value = if tuple_size(data) === 2, do: elem(data, 1), else: data - Agent.update(cache_name, fn state -> - Map.put(state, key, value) - end) - + scoped_agent_update(cache_name, fn sub -> Map.put(sub, key, value) end) true end def insert_raw(cache_name, data) when is_list(data) do - Agent.update(cache_name, fn state -> - Enum.reduce(data, state, fn tuple, acc -> + scoped_agent_update(cache_name, fn sub -> + Enum.reduce(data, sub, fn tuple, acc -> key = elem(tuple, 0) value = if tuple_size(tuple) === 2, do: elem(tuple, 1), else: tuple Map.put(acc, key, value) @@ -618,67 +670,61 @@ defmodule Cache.Sandbox do key = elem(data, 0) value = if tuple_size(data) === 2, do: elem(data, 1), else: data - Agent.get_and_update(cache_name, fn state -> - if Map.has_key?(state, key) do - {false, state} + scoped_agent_get_and_update(cache_name, fn sub -> + if Map.has_key?(sub, key) do + {false, sub} else - {true, Map.put(state, key, value)} + {true, Map.put(sub, key, value)} end end) end def insert_new(cache_name, data) when is_list(data) do - Agent.get_and_update(cache_name, fn state -> - keys_exist = Enum.any?(data, fn tuple -> Map.has_key?(state, elem(tuple, 0)) end) + scoped_agent_get_and_update(cache_name, fn sub -> + keys_exist = Enum.any?(data, fn tuple -> Map.has_key?(sub, elem(tuple, 0)) end) if keys_exist do - {false, state} + {false, sub} else - new_state = - Enum.reduce(data, state, fn tuple, acc -> + new_sub = + Enum.reduce(data, sub, fn tuple, acc -> key = elem(tuple, 0) value = if tuple_size(tuple) === 2, do: elem(tuple, 1), else: tuple Map.put(acc, key, value) end) - {true, new_state} + {true, new_sub} end end) end def take(cache_name, key) do - Agent.get_and_update(cache_name, fn state -> - case Map.pop(state, key) do - {nil, state} -> {[], state} - {value, new_state} -> {[{key, value}], new_state} + scoped_agent_get_and_update(cache_name, fn sub -> + case Map.pop(sub, key) do + {nil, sub} -> {[], sub} + {value, new_sub} -> {[{key, value}], new_sub} end end) end def tab2list(cache_name) do - Agent.get(cache_name, fn state -> - Map.to_list(state) - end) + scoped_agent_get(cache_name, fn sub -> Map.to_list(sub) end) end def match_object(cache_name, pattern) do - Agent.get(cache_name, fn state -> - state + scoped_agent_get(cache_name, fn sub -> + sub |> Map.to_list() - |> Enum.filter(fn {key, value} -> - match_pattern?({key, value}, pattern) - end) + |> Enum.filter(fn {key, value} -> match_pattern?({key, value}, pattern) end) end) end def match_object(cache_name, pattern, limit) do - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> results = - state + sub |> Map.to_list() - |> Enum.filter(fn {key, value} -> - match_pattern?({key, value}, pattern) - end) + |> Enum.filter(fn {key, value} -> match_pattern?({key, value}, pattern) end) |> Enum.take(limit) {results, :end_of_table} @@ -686,24 +732,20 @@ defmodule Cache.Sandbox do end def match_pattern(cache_name, pattern) do - Agent.get(cache_name, fn state -> - state + scoped_agent_get(cache_name, fn sub -> + sub |> Map.to_list() - |> Enum.filter(fn {key, value} -> - match_pattern?({key, value}, pattern) - end) + |> Enum.filter(fn {key, value} -> match_pattern?({key, value}, pattern) end) |> Enum.map(fn obj -> extract_bindings(obj, pattern) end) end) end def match_pattern(cache_name, pattern, limit) do - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> results = - state + sub |> Map.to_list() - |> Enum.filter(fn {key, value} -> - match_pattern?({key, value}, pattern) - end) + |> Enum.filter(fn {key, value} -> match_pattern?({key, value}, pattern) end) |> Enum.take(limit) |> Enum.map(fn obj -> extract_bindings(obj, pattern) end) @@ -720,9 +762,7 @@ defmodule Cache.Sandbox do object_list |> Enum.zip(pattern_list) - |> Enum.all?(fn {obj_elem, pat_elem} -> - match_element?(obj_elem, pat_elem) - end) + |> Enum.all?(fn {obj_elem, pat_elem} -> match_element?(obj_elem, pat_elem) end) else false end @@ -762,23 +802,19 @@ defmodule Cache.Sandbox do defp extract_bindings(_object, _pattern), do: [] def select(cache_name, match_spec) do - Agent.get(cache_name, fn state -> - state + scoped_agent_get(cache_name, fn sub -> + sub |> Map.to_list() - |> Enum.flat_map(fn {key, value} -> - apply_match_spec({key, value}, match_spec) - end) + |> Enum.flat_map(fn {key, value} -> apply_match_spec({key, value}, match_spec) end) end) end def select(cache_name, match_spec, limit) do - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> results = - state + sub |> Map.to_list() - |> Enum.flat_map(fn {key, value} -> - apply_match_spec({key, value}, match_spec) - end) + |> Enum.flat_map(fn {key, value} -> apply_match_spec({key, value}, match_spec) end) |> Enum.take(limit) {results, :end_of_table} @@ -786,9 +822,7 @@ defmodule Cache.Sandbox do end defp apply_match_spec(object, match_spec) when is_list(match_spec) do - Enum.flat_map(match_spec, fn spec -> - apply_single_match_spec(object, spec) - end) + Enum.flat_map(match_spec, fn spec -> apply_single_match_spec(object, spec) end) end # NOTE: Guards are not evaluated in the sandbox. Implementing a full guard @@ -873,8 +907,8 @@ defmodule Cache.Sandbox do defp resolve_bindings(value, _bindings), do: value def select_count(cache_name, match_spec) do - Agent.get(cache_name, fn state -> - state + scoped_agent_get(cache_name, fn sub -> + sub |> Map.to_list() |> Enum.count(fn {key, value} -> result = apply_match_spec({key, value}, match_spec) @@ -884,9 +918,9 @@ defmodule Cache.Sandbox do end def select_delete(cache_name, match_spec) do - Agent.get_and_update(cache_name, fn state -> + scoped_agent_get_and_update(cache_name, fn sub -> {to_delete, to_keep} = - state + sub |> Map.to_list() |> Enum.split_with(fn {key, value} -> result = apply_match_spec({key, value}, match_spec) @@ -898,11 +932,11 @@ defmodule Cache.Sandbox do end def select_replace(cache_name, match_spec) do - Agent.get_and_update(cache_name, fn state -> - {count, new_state} = - state + scoped_agent_get_and_update(cache_name, fn sub -> + {count, new_sub} = + sub |> Map.to_list() - |> Enum.reduce({0, state}, fn {key, value}, {cnt, acc} -> + |> Enum.reduce({0, sub}, fn {key, value}, {cnt, acc} -> result = apply_match_spec({key, value}, match_spec) case result do @@ -916,17 +950,15 @@ defmodule Cache.Sandbox do end end) - {count, new_state} + {count, new_sub} end) end def match_delete(cache_name, pattern) do - Agent.update(cache_name, fn state -> - state + scoped_agent_update(cache_name, fn sub -> + sub |> Map.to_list() - |> Enum.reject(fn {key, value} -> - match_pattern?({key, value}, pattern) - end) + |> Enum.reject(fn {key, value} -> match_pattern?({key, value}, pattern) end) |> Map.new() end) @@ -934,10 +966,10 @@ defmodule Cache.Sandbox do end def info(cache_name) do - Agent.get(cache_name, fn state -> + scoped_agent_get(cache_name, fn sub -> [ name: cache_name, - size: map_size(state), + size: map_size(sub), type: :set, named_table: true, keypos: 1, @@ -952,8 +984,8 @@ defmodule Cache.Sandbox do end def slot(cache_name, i) do - Agent.get(cache_name, fn state -> - list = Map.to_list(state) + scoped_agent_get(cache_name, fn sub -> + list = Map.to_list(sub) if i >= length(list) do :"$end_of_table" @@ -968,10 +1000,7 @@ defmodule Cache.Sandbox do end def init_table(cache_name, init_fun) do - Agent.update(cache_name, fn _state -> - read_init_fun(init_fun, %{}) - end) - + scoped_agent_update(cache_name, fn _sub -> read_init_fun(init_fun, %{}) end) true end @@ -1026,27 +1055,27 @@ defmodule Cache.Sandbox do end def traverse(cache_name, fun) do - Agent.get_and_update(cache_name, fn state -> - {results, new_state} = - state + scoped_agent_get_and_update(cache_name, fn sub -> + {results, new_sub} = + sub |> Map.to_list() - |> Enum.reduce({[], state}, fn {key, value}, {acc, current_state} -> + |> Enum.reduce({[], sub}, fn {key, value}, {acc, current_sub} -> case fun.({key, value}) do :continue -> - {acc, current_state} + {acc, current_sub} {:continue, result} -> - {[result | acc], current_state} + {[result | acc], current_sub} {:done, result} -> - {[result | acc], current_state} + {[result | acc], current_sub} :done -> - {acc, current_state} + {acc, current_sub} end end) - {Enum.reverse(results), new_state} + {Enum.reverse(results), new_sub} end) end @@ -1142,33 +1171,33 @@ defmodule Cache.Sandbox do end def update_element(cache_name, key, element_spec) do - Agent.get_and_update(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get_and_update(cache_name, fn sub -> + case Map.get(sub, key) do nil -> - {false, state} + {false, sub} value when is_tuple(value) -> new_value = apply_element_spec(value, element_spec) - {true, Map.put(state, key, new_value)} + {true, Map.put(sub, key, new_value)} _ -> - {false, state} + {false, sub} end end) end def update_element(cache_name, key, element_spec, default) do - Agent.get_and_update(cache_name, fn state -> - case Map.get(state, key) do + scoped_agent_get_and_update(cache_name, fn sub -> + case Map.get(sub, key) do nil -> - {true, Map.put(state, key, default)} + {true, Map.put(sub, key, default)} value when is_tuple(value) -> new_value = apply_element_spec(value, element_spec) - {true, Map.put(state, key, new_value)} + {true, Map.put(sub, key, new_value)} _ -> - {false, state} + {false, sub} end end) end @@ -1201,6 +1230,7 @@ defmodule Cache.Sandbox do raise "Not Implemented" end + # SECTION: internal helpers defp check_key_exists(state, key) do if Map.has_key?(state, key) do @@ -1242,16 +1272,6 @@ defmodule Cache.Sandbox do defp apply_scan_count(values, nil), do: values defp apply_scan_count(values, count) when is_integer(count), do: Enum.take(values, count) - defp scan_key(key) do - key - |> to_string() - |> String.split(":", parts: 2) - |> case do - [_prefix, rest] -> rest - [value] -> value - end - end - defp scan_match?(value, pattern) do value |> to_string() @@ -1302,8 +1322,8 @@ defmodule Cache.Sandbox do defp serialize_path_and_get_value(cache_name, key, path) do path = JSON.serialize_path(path) - Agent.get(cache_name, fn state -> - case get_in(state, [key | String.split(path, ".")]) do + scoped_agent_get(cache_name, fn sub -> + case get_in(sub, [key | String.split(path, ".")]) do nil -> {:error, ErrorMessage.not_found("ERR Path '$.#{path}' does not exist")} value -> {:ok, value} end diff --git a/test/cache_sandbox_test.exs b/test/cache_sandbox_test.exs index 5c45ebd..541bfe8 100644 --- a/test/cache_sandbox_test.exs +++ b/test/cache_sandbox_test.exs @@ -39,6 +39,28 @@ defmodule CacheSandboxTest do test "adapter gets swapped to sandbox adapter" do assert TestCache.cache_adapter() === Cache.Sandbox end + + test "scan only returns keys from the current sandbox" do + assert :ok = TestCache.put("alpha", 1) + assert :ok = TestCache.put("beta", 2) + + task = + Task.async(fn -> + Cache.SandboxRegistry.register_caches(TestCache) + :ok = TestCache.put("gamma", 3) + TestCache.scan() + end) + + assert {:ok, other_keys} = Task.await(task) + assert "gamma" in other_keys + refute "alpha" in other_keys + refute "beta" in other_keys + + assert {:ok, my_keys} = TestCache.scan() + assert "alpha" in my_keys + assert "beta" in my_keys + refute "gamma" in my_keys + end end describe "&json_get/1" do