From 521b61095bbbc779a815c12ef2141f25651367ba Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 18:02:57 +0200 Subject: [PATCH] feat: add Excache.Config and names --- lib/excache/config.ex | 208 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 lib/excache/config.ex diff --git a/lib/excache/config.ex b/lib/excache/config.ex new file mode 100644 index 0000000..84a47cd --- /dev/null +++ b/lib/excache/config.ex @@ -0,0 +1,208 @@ +defmodule Excache.Config do + @moduledoc false + + alias Excache.Aof + alias Excache.Eviction + alias Excache.Eviction.Runner + alias Excache.Persistence + + @defaults %{ + maxmemory: 0, + maxmemoryPolicy: :noeviction, + maxmemorySamples: 5, + lfuLogFactor: 10, + lfuDecayTime: 1, + port: 6379, + snapshot_path: nil, + aof_path: nil + } + + @parameters %{ + "maxmemory" => %{env: :maxmemory, type: :bytes, mutable: true, runtime: true}, + "maxmemory-policy" => %{env: :maxmemoryPolicy, type: :policy, mutable: true, runtime: true}, + "maxmemory-samples" => %{ + env: :maxmemorySamples, + type: {:bounded, 1, 64}, + mutable: true, + runtime: true + }, + "lfu-log-factor" => %{ + env: :lfuLogFactor, + type: {:bounded, 0, 255}, + mutable: true, + runtime: true + }, + "lfu-decay-time" => %{ + env: :lfuDecayTime, + type: {:bounded, 0, 1440}, + mutable: true, + runtime: true + }, + "port" => %{env: :port, type: {:bounded, 0, 65_535}, mutable: false, runtime: false}, + "snapshot_path" => %{env: :snapshot_path, type: :path, mutable: true, runtime: false}, + "aof_path" => %{env: :aof_path, type: :path, mutable: false, runtime: false} + } + + @trackedPolicies [:allkeysLru, :allkeysLfu, :volatileLru, :volatileLfu] + + @units %{ + "" => 1, + "b" => 1, + "k" => 1000, + "kb" => 1024, + "m" => 1_000_000, + "mb" => 1_048_576, + "g" => 1_000_000_000, + "gb" => 1_073_741_824 + } + + def names, do: @parameters |> Map.keys() |> Enum.sort() + + def get(name) do + case Map.fetch(@parameters, String.downcase(name)) do + {:ok, spec} -> {:ok, readSpec(spec)} + :error -> :error + end + end + + def set(name, raw) do + case prepare(name, raw) do + {:ok, change} -> commit(change) + {:error, reason} -> {:error, reason} + end + end + + def setMany(pairs) do + pairs + |> Enum.reduce_while({:ok, []}, fn {name, raw}, {:ok, acc} -> + case prepare(name, raw) do + {:ok, change} -> {:cont, {:ok, [change | acc]}} + {:error, reason} -> {:halt, {:error, name, reason}} + end + end) + |> case do + {:ok, changes} -> + changes |> Enum.reverse() |> Enum.each(&commit/1) + :ok + + failure -> + failure + end + end + + def getGlob(pattern) do + chars = pattern |> String.downcase() |> String.to_charlist() + + for name <- names(), matches?(chars, String.to_charlist(name)) do + {:ok, value} = get(name) + {name, format(value)} + end + end + + def reset do + for {_name, spec} <- @parameters, spec.runtime do + Application.delete_env(:excache, spec.env) + end + + Runner.invalidate() + Runner.reloadTracker() + :ok + end + + defp format(nil), do: "" + defp format(value) when is_integer(value), do: Integer.to_string(value) + defp format(value) when is_binary(value), do: value + defp format(policy) when is_atom(policy), do: Eviction.policyName(policy) + + def maxmemory, do: readEnv(:maxmemory) + def policy, do: readEnv(:maxmemoryPolicy) + def maxmemorySamples, do: readEnv(:maxmemorySamples) + def lfuLogFactor, do: readEnv(:lfuLogFactor) + def lfuDecayTime, do: readEnv(:lfuDecayTime) + + def tracksAccess?, do: policy() in @trackedPolicies + + defp hint(:bytes), do: "argument must be a memory value like 512, 64kb, 100mb or 1gb" + + defp hint(:policy), + do: + "argument must be one of the following: " <> + Enum.map_join(Eviction.policies(), ", ", &Eviction.policyName/1) + + defp hint({:bounded, low, high}), do: "argument must be an integer between #{low} and #{high}" + defp hint(:path), do: "argument must be a non empty path" + + defp prepare(name, raw) do + case Map.fetch(@parameters, String.downcase(name)) do + :error -> + {:error, :unknown} + + {:ok, %{mutable: false}} -> + {:error, :immutable} + + {:ok, spec} -> + case parse(spec.type, raw) do + {:ok, value} -> {:ok, {spec, value}} + :error -> {:error, {:invalid, hint(spec.type)}} + end + end + end + + defp commit({spec, value}) do + previous = readSpec(spec) + Application.put_env(:excache, spec.env, value) + if value != previous, do: applyChange(spec) + :ok + end + + defp applyChange(%{env: env}) when env in [:lfuLogFactor, :lfuDecayTime], + do: Runner.reloadTracker() + + defp applyChange(%{env: env}) when env in [:maxmemory, :maxmemoryPolicy, :maxmemorySamples], + do: Runner.invalidate() + + defp applyChange(_spec), do: :ok + + defp parse(:bytes, raw), do: parseBytes(raw) + defp parse(:policy, raw), do: Eviction.parsePolicy(String.downcase(raw)) + + defp parse({:bounded, low, high}, raw) do + case Integer.parse(raw) do + {value, ""} when value >= low and value <= high -> {:ok, value} + _ -> :error + end + end + + defp parse(:path, ""), do: :error + defp parse(:path, raw), do: {:ok, raw} + + defp parseBytes(raw) do + {digits, unit} = + raw + |> String.downcase() + |> String.to_charlist() + |> Enum.split_while(&(&1 in ?0..?9)) + + with {:ok, factor} <- Map.fetch(@units, List.to_string(unit)), + {amount, ""} <- Integer.parse(List.to_string(digits)) do + {:ok, amount * factor} + else + _ -> :error + end + end + + defp readSpec(%{env: :snapshot_path}), do: Persistence.snapshot_path() + defp readSpec(%{env: :aof_path}), do: Aof.path() + defp readSpec(%{env: env}), do: readEnv(env) + + defp readEnv(env), do: Application.get_env(:excache, env, @defaults[env]) + + defp matches?([], name), do: name == [] + + defp matches?([?* | pattern], name), + do: matches?(pattern, name) or (name != [] and matches?([?* | pattern], tl(name))) + + defp matches?([?? | pattern], [_char | name]), do: matches?(pattern, name) + defp matches?([char | pattern], [char | name]), do: matches?(pattern, name) + defp matches?(_pattern, _name), do: false +end