diff --git a/lib/excache/eviction.ex b/lib/excache/eviction.ex index 7e2e7bf..18c2c2d 100644 --- a/lib/excache/eviction.ex +++ b/lib/excache/eviction.ex @@ -155,3 +155,105 @@ defmodule Excache.Eviction do {sampled, randState} = config.sampler.(remaining, config.sampleSize, config.randState) config = %{config | randState: randState} + if random?(config.policy) do + {hd(sampled), pool, config} + else + metadata = config.metaFun.(Enum.map(sampled, & &1.key), config.now) + + scored = + Enum.map(sampled, fn candidate -> + {score(config.policy, candidate, Map.get(metadata, candidate.key), config.now), + candidate} + end) + + [{_score, victim} | rest] = mergePool(pool, scored, config.poolSize) + {victim, rest, config} + end + end + + defp mergePool(pool, scored, poolSize) do + (scored ++ pool) + |> Enum.uniq_by(fn {_score, candidate} -> candidate.key end) + |> Enum.sort_by(fn {candidateScore, _candidate} -> candidateScore end, :desc) + |> Enum.take(poolSize) + end + + defp score(policy, _candidate, meta, now) when policy in @lruPolicies, do: lruScore(meta, now) + + defp score(policy, _candidate, meta, _now) when policy in @lfuPolicies, + do: @lfuMaxCounter - counterOf(meta) + + defp score(:volatileTtl, candidate, _meta, _now), do: -candidate.expiresAt + + defp lruScore(nil, _now), do: {1, 0} + defp lruScore(meta, now), do: {0, max(now - meta.lastAccess, 0)} + + defp counterOf(nil), do: 0 + defp counterOf(meta), do: meta.counter + + defp eligible?(_candidate, :noeviction), do: false + + defp eligible?(candidate, policy) do + if volatile?(policy), do: candidate.expiresAt != nil, else: true + end + + defp emptyReason(policy) do + if volatile?(policy), do: :noVolatileKeys, else: :noCandidates + end + + defp configure(opts) do + %{ + policy: Keyword.fetch!(opts, :policy), + maxmemory: Keyword.get(opts, :maxmemory, 0), + now: Keyword.get_lazy(opts, :now, fn -> System.system_time(:millisecond) end), + sampleSize: Keyword.get(opts, :sampleSize, @defaultSampleSize), + poolSize: Keyword.get(opts, :poolSize, @defaultPoolSize), + sampler: Keyword.get(opts, :sampler, &sampleRandom/3), + randState: :rand.seed_s(:exsss, Keyword.get(opts, :seed, @defaultSeed)), + tracker: Keyword.get(opts, :tracker), + metaFun: metaFun(opts) + } + end + + defp metaFun(opts) do + cond do + Keyword.has_key?(opts, :metadata) -> + metadata = Keyword.fetch!(opts, :metadata) + fn keys, _now -> Map.take(metadata, keys) end + + Keyword.has_key?(opts, :tracker) -> + tracker = Keyword.fetch!(opts, :tracker) + fn keys, now -> Tracker.metadata(tracker, keys, now) end + + true -> + fn _keys, _now -> %{} end + end + end + + defp sampleRandom(pool, count, randState) when length(pool) <= count, do: {pool, randState} + + defp sampleRandom(pool, count, randState) do + {taken, _rest, _size, nextState} = + Enum.reduce(1..count, {[], pool, length(pool), randState}, fn _index, + {acc, left, size, state} -> + {index, drawn} = :rand.uniform_s(size, state) + {picked, rest} = List.pop_at(left, index - 1) + {[picked | acc], rest, size - 1, drawn} + end) + + {Enum.reverse(taken), nextState} + end + + defp report(config, acc, reason) do + %{ + reason: reason, + policy: config.policy, + maxmemory: config.maxmemory, + usedMemory: acc.used, + freedBytes: acc.freed, + evicted: Enum.reverse(acc.evicted), + evictedCount: length(acc.evicted), + passes: acc.passes + } + end +end