Skip to content

Commit

Permalink
[#219] Multilevel inclusive replicates entries to backward levels by …
Browse files Browse the repository at this point in the history
…default
  • Loading branch information
cabol committed Jan 14, 2024
1 parent 0886eff commit 387d82e
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 15 deletions.
52 changes: 37 additions & 15 deletions lib/nebulex/adapters/multilevel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,19 @@ defmodule Nebulex.Adapters.Multilevel do
defaults to `:inclusive`. In an inclusive cache, the same data can be
present in all caches/levels. In an exclusive cache, data can be present
in only one cache/level and a key cannot be found in the rest of caches
at the same time. This option affects `get` operation only; if
`:cache_model` is `:inclusive`, when the key is found in a level N,
at the same time. This option applies to the `get` callabck only; if the
cache `:model` is `:inclusive`, when the key is found in a level N,
that entry is duplicated backwards (to all previous levels: 1..N-1).
However, when the mode is set to `:inclusive`, the `get_all` operation
is translated into multiple `get` calls underneath (which may be a
significant performance penalty) since is required to replicate the
entries properly with their current TTLs. It is possible to skip the
replication when calling `get_all` using the option `:replicate`.
* `:replicate` - This option applies only to the `get_all` callback.
Determines whether the entries should be replicated to the backward
levels or not. Defaults to `true`.
## Shared options
Expand Down Expand Up @@ -312,37 +322,47 @@ defmodule Nebulex.Adapters.Multilevel do

@impl true
defspan get(adapter_meta, key, opts) do
fun = fn level, {default, prev} ->
opts
|> levels(adapter_meta.levels)
|> Enum.reduce_while({nil, []}, fn level, {default, prev} ->
value = with_dynamic_cache(level, :get, [key, opts])

if is_nil(value) do
{:cont, {default, [level | prev]}}
else
{:halt, {value, [level | prev]}}
end
end

opts
|> levels(adapter_meta.levels)
|> Enum.reduce_while({nil, []}, fun)
end)
|> maybe_replicate(key, adapter_meta.model)
end

@impl true
defspan get_all(adapter_meta, keys, opts) do
fun = fn level, {keys_acc, map_acc} ->
{replicate?, opts} = Keyword.pop(opts, :replicate, true)

do_get_all(adapter_meta, keys, replicate?, opts)
end

defp do_get_all(%{model: :inclusive} = adapter_meta, keys, true, opts) do
Enum.reduce(keys, %{}, fn key, acc ->
if obj = get(adapter_meta, key, opts),
do: Map.put(acc, key, obj),
else: acc
end)
end

defp do_get_all(%{levels: levels}, keys, _replicate?, opts) do
opts
|> levels(levels)
|> Enum.reduce_while({keys, %{}}, fn level, {keys_acc, map_acc} ->
map = with_dynamic_cache(level, :get_all, [keys_acc, opts])
map_acc = Map.merge(map_acc, map)

case keys_acc -- Map.keys(map) do
[] -> {:halt, {[], map_acc}}
keys_acc -> {:cont, {keys_acc, map_acc}}
end
end

opts
|> levels(adapter_meta.levels)
|> Enum.reduce_while({keys, %{}}, fun)
end)
|> elem(1)
end

Expand Down Expand Up @@ -591,7 +611,9 @@ defmodule Nebulex.Adapters.Multilevel do
end
end

defp maybe_replicate({nil, _}, _, _), do: nil
defp maybe_replicate({nil, _}, _, _) do
nil
end

defp maybe_replicate({value, [level_meta | [_ | _] = levels]}, key, :inclusive) do
ttl = with_dynamic_cache(level_meta, :ttl, [key]) || :infinity
Expand Down
50 changes: 50 additions & 0 deletions test/nebulex/adapters/multilevel_inclusive_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,57 @@ defmodule Nebulex.Adapters.MultilevelInclusiveTest do
assert Multilevel.get(3) == 3
assert Multilevel.get(3, level: 1) == 3
assert Multilevel.get(3, level: 2) == 3
assert Multilevel.get(3, level: 3) == 3
end

test "get_all [replicate: true]" do
:ok = Process.sleep(2000)
:ok = Multilevel.put(1, 1, level: 1)
:ok = Multilevel.put(2, 2, level: 2)
:ok = Multilevel.put(3, 3, level: 3)

assert Multilevel.get_all([1]) == %{1 => 1}
refute Multilevel.get(1, level: 2)
refute Multilevel.get(1, level: 3)

assert Multilevel.get_all([1, 2]) == %{1 => 1, 2 => 2}
assert Multilevel.get(2, level: 1) == 2
assert Multilevel.get(2, level: 2) == 2
refute Multilevel.get(2, level: 3)

assert Multilevel.get(3, level: 3) == 3
refute Multilevel.get(3, level: 1)
refute Multilevel.get(3, level: 2)

assert Multilevel.get_all([1, 2, 3]) == %{1 => 1, 2 => 2, 3 => 3}
assert Multilevel.get(3, level: 1) == 3
assert Multilevel.get(3, level: 2) == 3
assert Multilevel.get(3, level: 3) == 3
end

test "get_all [replicate: false]" do
:ok = Process.sleep(2000)
:ok = Multilevel.put(1, 1, level: 1)
:ok = Multilevel.put(2, 2, level: 2)
:ok = Multilevel.put(3, 3, level: 3)

assert Multilevel.get_all([1], replicate: false) == %{1 => 1}
refute Multilevel.get(1, level: 2)
refute Multilevel.get(1, level: 3)

assert Multilevel.get_all([1, 2], replicate: false) == %{1 => 1, 2 => 2}
refute Multilevel.get(2, level: 1)
assert Multilevel.get(2, level: 2) == 2
refute Multilevel.get(2, level: 3)

assert Multilevel.get(3, level: 3) == 3
refute Multilevel.get(3, level: 1)
refute Multilevel.get(3, level: 2)

assert Multilevel.get_all([1, 2, 3], replicate: false) == %{1 => 1, 2 => 2, 3 => 3}
refute Multilevel.get(3, level: 1)
refute Multilevel.get(3, level: 2)
assert Multilevel.get(3, level: 3) == 3
end

test "get boolean" do
Expand Down

0 comments on commit 387d82e

Please sign in to comment.