feat(eviction): add Excache.Eviction.Runner and start_link
This commit is contained in:
1 parent
c4ac5371e6
commit
595d03fe96
1 file changed
+208
@@ -0,0 +1,208 @@
|
|||||||
|
defmodule Excache.Eviction.Runner do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
alias Excache.Aof
|
||||||
|
alias Excache.Config
|
||||||
|
alias Excache.Eviction
|
||||||
|
alias Excache.Eviction.Tracker
|
||||||
|
alias Excache.Memory
|
||||||
|
alias Excache.Notify
|
||||||
|
alias Excache.Store
|
||||||
|
|
||||||
|
@meter {__MODULE__, :meter}
|
||||||
|
@used 1
|
||||||
|
@dirty 2
|
||||||
|
@checkedAt 3
|
||||||
|
@verdict 4
|
||||||
|
@slots 4
|
||||||
|
|
||||||
|
@defaultInterval 200
|
||||||
|
@defaultRecheck 20
|
||||||
|
|
||||||
|
def start_link(opts \\ []), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
|
|
||||||
|
def admit do
|
||||||
|
limit = Config.maxmemory()
|
||||||
|
if limit <= 0, do: :ok, else: decide(limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account(args) do
|
||||||
|
if Config.maxmemory() > 0, do: :counters.add(meter(), @dirty, argBytes(args))
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def enforce, do: GenServer.call(__MODULE__, :enforce, 30_000)
|
||||||
|
|
||||||
|
def invalidate do
|
||||||
|
meter = meter()
|
||||||
|
:counters.put(meter, @dirty, 0)
|
||||||
|
:counters.put(meter, @checkedAt, 0)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def reloadTracker, do: GenServer.call(__MODULE__, :reloadTracker)
|
||||||
|
|
||||||
|
def stats, do: GenServer.call(__MODULE__, :stats)
|
||||||
|
|
||||||
|
def resetStats, do: GenServer.call(__MODULE__, :resetStats)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_opts) do
|
||||||
|
:persistent_term.put(@meter, :counters.new(@slots, [:write_concurrency]))
|
||||||
|
schedule()
|
||||||
|
{:ok, %{evictedKeys: 0, oomRejections: 0}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:enforce, _from, state) do
|
||||||
|
{verdict, state} = sweep(state)
|
||||||
|
{:reply, verdict, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:reloadTracker, _from, state) do
|
||||||
|
wanted = %{logFactor: Config.lfuLogFactor(), decayTime: Config.lfuDecayTime()}
|
||||||
|
if Tracker.settings(Tracker) != wanted, do: restartTracker()
|
||||||
|
{:reply, :ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:stats, _from, state) do
|
||||||
|
{:reply, %{evictedKeys: state.evictedKeys, oomRejections: state.oomRejections}, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:resetStats, _from, state),
|
||||||
|
do: {:reply, :ok, %{state | evictedKeys: 0, oomRejections: 0}}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:tick, state) do
|
||||||
|
state = maintain(state)
|
||||||
|
schedule()
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maintain(state) do
|
||||||
|
cond do
|
||||||
|
Config.maxmemory() > 0 ->
|
||||||
|
elem(sweep(state), 1)
|
||||||
|
|
||||||
|
Config.tracksAccess?() ->
|
||||||
|
pruneMetadata(Store.dbsize(), fn -> Store.keys("*") end)
|
||||||
|
state
|
||||||
|
|
||||||
|
true ->
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decide(limit) do
|
||||||
|
meter = meter()
|
||||||
|
checkedAt = :counters.get(meter, @checkedAt)
|
||||||
|
estimated = :counters.get(meter, @used) + :counters.get(meter, @dirty)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
checkedAt == 0 -> enforce()
|
||||||
|
estimated <= limit -> :ok
|
||||||
|
recent?(checkedAt) -> cached(meter)
|
||||||
|
true -> enforce()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp recent?(checkedAt) do
|
||||||
|
window = Application.get_env(:excache, :eviction_recheck, @defaultRecheck)
|
||||||
|
window > 0 and System.system_time(:millisecond) - checkedAt < window
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cached(meter), do: if(:counters.get(meter, @verdict) == 0, do: :ok, else: :oom)
|
||||||
|
|
||||||
|
defp sweep(state) do
|
||||||
|
entries = Store.dump()
|
||||||
|
pruneMetadata(length(entries), fn -> Enum.map(entries, &elem(&1, 0)) end)
|
||||||
|
|
||||||
|
used = Memory.usedMemory(entries)
|
||||||
|
limit = Config.maxmemory()
|
||||||
|
policy = Config.policy()
|
||||||
|
|
||||||
|
cond do
|
||||||
|
limit <= 0 or used <= limit ->
|
||||||
|
record(used, :ok)
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
policy == :noeviction ->
|
||||||
|
record(used, :oom)
|
||||||
|
{:oom, %{state | oomRejections: state.oomRejections + 1}}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
evict(entries, policy, limit, state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp evict(entries, policy, limit, state) do
|
||||||
|
{status, report} =
|
||||||
|
Eviction.run(
|
||||||
|
policy: policy,
|
||||||
|
maxmemory: limit,
|
||||||
|
sampleSize: Config.maxmemorySamples(),
|
||||||
|
tracker: Tracker,
|
||||||
|
keyspace: keyspace(entries)
|
||||||
|
)
|
||||||
|
|
||||||
|
verdict = if status == :ok, do: :ok, else: :oom
|
||||||
|
record(report.usedMemory, verdict)
|
||||||
|
|
||||||
|
state = %{
|
||||||
|
state
|
||||||
|
| evictedKeys: state.evictedKeys + report.evictedCount,
|
||||||
|
oomRejections: state.oomRejections + if(verdict == :oom, do: 1, else: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
{verdict, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp keyspace(entries) do
|
||||||
|
%{
|
||||||
|
entries: fn -> entries end,
|
||||||
|
delete: fn key ->
|
||||||
|
deleted = Store.del([key])
|
||||||
|
Aof.append("DEL", [key])
|
||||||
|
Notify.key_event(key, "evicted")
|
||||||
|
deleted
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pruneMetadata(keyCount, keysFun) do
|
||||||
|
if Config.tracksAccess?() and Tracker.size(Tracker) > keyCount do
|
||||||
|
Tracker.retain(Tracker, keysFun.())
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restartTracker do
|
||||||
|
Supervisor.terminate_child(Excache.Supervisor, Tracker)
|
||||||
|
Supervisor.delete_child(Excache.Supervisor, Tracker)
|
||||||
|
Supervisor.start_child(Excache.Supervisor, Excache.Application.trackerSpec())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp record(used, verdict) do
|
||||||
|
meter = meter()
|
||||||
|
:counters.put(meter, @used, used)
|
||||||
|
:counters.put(meter, @dirty, 0)
|
||||||
|
:counters.put(meter, @checkedAt, System.system_time(:millisecond))
|
||||||
|
:counters.put(meter, @verdict, if(verdict == :ok, do: 0, else: 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp argBytes(args),
|
||||||
|
do: Enum.reduce(args, Memory.entryOverhead(), fn arg, total -> total + byte_size(arg) end)
|
||||||
|
|
||||||
|
defp meter, do: :persistent_term.get(@meter)
|
||||||
|
|
||||||
|
defp schedule do
|
||||||
|
interval = Application.get_env(:excache, :eviction_interval, @defaultInterval)
|
||||||
|
Process.send_after(self(), :tick, interval)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user