test: cover mixedEntries and mixedMetadata

This commit is contained in:
dualfroz committed 2026-09-10 15:41:53 +02:00
1 parent b65f902d27
commit 6c9922f88b
1 file changed
+226
+226
View File
@@ -152,3 +152,229 @@ defmodule Excache.EvictionTest do
"k4" => %{lastAccess: @now - 30_000, counter: 9} "k4" => %{lastAccess: @now - 30_000, counter: 9}
} }
assert {:ok, report} =
Eviction.run(
policy: :allkeysLru,
maxmemory: 2 * perEntry,
keyspace: keyspace,
now: @now,
metadata: metadata,
sampleSize: 2,
sampler: windowSampler()
)
assert report.evicted == ["k1", "k2"]
end
test "sampling scores a window instead of the whole keyspace" do
entries = stringEntries(~w(k1 k2 k3 k4), "payload")
metadata = %{
"k1" => %{lastAccess: @now - 1_000, counter: 9},
"k2" => %{lastAccess: @now - 2_000, counter: 9},
"k3" => %{lastAccess: @now - 3_000, counter: 9},
"k4" => %{lastAccess: @now - 900_000, counter: 9}
}
opts = [now: @now, metadata: metadata, sampler: firstSampler()]
assert Eviction.selectVictim(entries, [policy: :allkeysLru, sampleSize: 1] ++ opts) ==
{:ok, "k1"}
assert Eviction.selectVictim(entries, [policy: :allkeysLru, sampleSize: 4] ++ opts) ==
{:ok, "k4"}
end
test "volatile-ttl evicts in expiry order and ignores keys without a TTL" do
entries = [
{"far", :string, "payload", @now + 900_000},
{"immortal", :string, "payload", nil},
{"near", :string, "payload", @now + 1_000},
{"mid", :string, "payload", @now + 60_000}
]
perEntry = Memory.entryBytes(hd(entries))
{table, keyspace} = fixtureKeyspace(entries)
assert {:ok, report} =
Eviction.run(
policy: :volatileTtl,
maxmemory: 2 * perEntry,
keyspace: keyspace,
now: @now,
sampleSize: 8
)
assert report.evicted == ["near", "mid"]
assert :ets.lookup(table, "immortal") != []
end
test "volatile candidates can run out before the limit is reached" do
entries =
stringEntries(~w(a1 a2 a3), "payload") ++
[
{"t1", :string, "payload", @now + 900_000},
{"t2", :string, "payload", @now + 900_000}
]
{table, keyspace} = fixtureKeyspace(entries)
assert {:error, report} =
Eviction.run(
policy: :volatileRandom,
maxmemory: 1,
keyspace: keyspace,
now: @now,
sampleSize: 8
)
assert report.reason == :exhausted
assert Enum.sort(report.evicted) == ["t1", "t2"]
assert report.usedMemory > 1
assert :ets.info(table, :size) == 3
end
test "allkeys-lfu backed by a live tracker keeps the hot key and forgets the victim" do
tracker = startTracker(logFactor: 0, decayTime: 0)
Enum.each(1..50, fn _index -> Tracker.touch(tracker, "hot", @now) end)
Tracker.touch(tracker, "cold", @now)
entries = stringEntries(["cold", "hot"], "payload")
coldBytes = Memory.entryBytes(hd(entries))
{table, keyspace} = fixtureKeyspace(entries)
assert {:ok, report} =
Eviction.run(
policy: :allkeysLfu,
maxmemory: Memory.usedMemory(entries) - coldBytes,
keyspace: keyspace,
tracker: tracker,
now: @now,
sampleSize: 8
)
assert report.evicted == ["cold"]
assert :ets.lookup(table, "hot") != []
assert Tracker.lookup(tracker, "cold", @now) == nil
assert Tracker.counter(tracker, "hot", @now) == 54
end
test "volatile-lru backed by a live tracker evicts the idle TTL key" do
tracker = startTracker(logFactor: 0, decayTime: 0)
Tracker.touch(tracker, "stale", @now - 300_000)
Tracker.touch(tracker, "fresh", @now - 1_000)
Tracker.touch(tracker, "idlest", @now - 900_000)
entries = [
{"fresh", :string, "payload", @now + 900_000},
{"stale", :string, "payload", @now + 900_000},
{"idlest", :string, "payload", nil}
]
assert Eviction.selectVictim(entries,
policy: :volatileLru,
tracker: tracker,
now: @now,
sampleSize: 8
) == {:ok, "stale"}
assert Eviction.selectVictim(entries,
policy: :allkeysLru,
tracker: tracker,
now: @now,
sampleSize: 8
) == {:ok, "idlest"}
end
test "a run against the live store really removes the keys" do
for index <- 1..20 do
:ok = Store.set("evict:#{index}", String.duplicate("payload", 20), [])
end
used = Memory.usedMemory()
target = div(used, 2)
assert {:ok, report} =
Eviction.run(policy: :allkeysRandom, maxmemory: target, seed: {1, 2, 3})
assert report.reason == :evicted
assert report.evictedCount >= 10
assert Memory.usedMemory() <= target
assert Memory.usedMemory() == report.usedMemory
assert Store.dbsize() == 20 - report.evictedCount
for key <- report.evicted do
assert Store.get(key) == nil
end
end
test "candidates carry the estimated size of the entry they stand for" do
entries = [
{"small", :string, "v", nil},
{"large", :string, :binary.copy("z", 4096), @now + 1_000}
]
[small, large] = Eviction.candidates(entries, :allkeysLru)
assert small.bytes == Memory.entryBytes({"small", :string, "v", nil})
assert large.bytes > 4096
assert large.expiresAt == @now + 1_000
assert Eviction.candidates(entries, :volatileLru) |> Enum.map(& &1.key) == ["large"]
assert Eviction.candidates(entries, :noeviction) == []
end
defp mixedEntries do
[
{"random-all", :string, "payload", nil},
{"idle-all", :string, "payload", nil},
{"cold-all", :string, "payload", nil},
{"random-ttl", :string, "payload", @now + 900_000},
{"idle-ttl", :string, "payload", @now + 900_000},
{"cold-ttl", :string, "payload", @now + 900_000},
{"soon-ttl", :string, "payload", @now + 1_000},
{"filler", :string, "payload", nil}
]
end
defp mixedMetadata do
%{
"random-all" => %{lastAccess: @now - 1_000, counter: 200},
"idle-all" => %{lastAccess: @now - 100_000, counter: 200},
"cold-all" => %{lastAccess: @now - 1_000, counter: 1},
"random-ttl" => %{lastAccess: @now - 1_000, counter: 200},
"idle-ttl" => %{lastAccess: @now - 90_000, counter: 200},
"cold-ttl" => %{lastAccess: @now - 1_000, counter: 2},
"soon-ttl" => %{lastAccess: @now - 1_000, counter: 200},
"filler" => %{lastAccess: @now - 500, counter: 250}
}
end
defp stringEntries(keys, value), do: Enum.map(keys, fn key -> {key, :string, value, nil} end)
defp fixtureKeyspace(entries) do
table = :ets.new(:evictionFixture, [:ordered_set, :public])
Enum.each(entries, &:ets.insert(table, &1))
{table,
%{
entries: fn -> :ets.tab2list(table) end,
delete: fn key -> :ets.delete(table, key) end
}}
end
defp firstSampler, do: fn pool, count, randState -> {Enum.take(pool, count), randState} end
defp windowSampler do
{:ok, cursor} = Agent.start_link(fn -> 0 end)
fn pool, count, randState ->
offset = Agent.get_and_update(cursor, fn pass -> {pass, pass + 1} end)
{pool |> Enum.drop(offset) |> Enum.take(count), randState}
end
end
defp startTracker(opts) do
id = {Tracker, System.unique_integer([:positive])}
start_supervised!(Supervisor.child_spec({Tracker, opts}, id: id))
end
end