defmodule Cache.ETS do @opts_definition [ write_concurrency: [ type: :boolean, doc: "Enable write concurrency" ], read_concurrency: [ type: :boolean, doc: "Enable read concurrency" ], decentralized_counters: [ type: :boolean, doc: "Use decentralized counters" ], type: [ type: {:in, [:bag, :duplicate_bag, :set]}, default: :set, doc: "Data type of ETS cache" ], compressed: [ type: :boolean, doc: "Enable ets compression" ] ] @moduledoc """ ETS (Erlang Term Storage) adapter for high-performance in-memory caching. This adapter provides a fast, process-independent cache using Erlang's built-in ETS tables. It's ideal for applications requiring low-latency access to cached data within a single node. ## Features * In-memory storage with configurable concurrency options * Direct access to ETS-specific operations * Very high performance for read and write operations * Support for atomic counter operations ## Options #{NimbleOptions.docs(@opts_definition)} ## Example ```elixir defmodule MyApp.Cache do use Cache, adapter: Cache.ETS, name: :my_app_cache, opts: [ read_concurrency: true, write_concurrency: true ] end ``` """ use Task, restart: :permanent @behaviour Cache defmacro __using__(_opts) do quote do @doc """ Match objects in the ETS table that match the given pattern. ## Examples iex> #{inspect(__MODULE__)}.match_object({:_, :_}) [...] """ def match_object(pattern) do :ets.match_object(@cache_name, pattern) end @doc """ Match objects in the ETS table that match the given pattern with limit. ## Examples iex> #{inspect(__MODULE__)}.match_object({:_, :_}, 10) {[...], continuation} """ def match_object(pattern, limit) do :ets.match_object(@cache_name, pattern, limit) end @doc """ Check if a key is a member of the ETS table. ## Examples iex> #{inspect(__MODULE__)}.member(:key) true """ def member(key) do :ets.member(@cache_name, key) end @doc """ Select objects from the ETS table using a match specification. ## Examples iex> #{inspect(__MODULE__)}.select([{{:key, :_}, [], [:'$_']}]) [...] """ def select(match_spec) do :ets.select(@cache_name, match_spec) end @doc """ Select objects from the ETS table using a match specification with limit. ## Examples iex> #{inspect(__MODULE__)}.select([{{:key, :_}, [], [:'$_']}], 10) {[...], continuation} """ def select(match_spec, limit) do :ets.select(@cache_name, match_spec, limit) end @doc """ Get information about the ETS table. ## Examples iex> #{inspect(__MODULE__)}.info() [...] """ def info do :ets.info(@cache_name) end @doc """ Get specific information about the ETS table. ## Examples iex> #{inspect(__MODULE__)}.info(:size) 42 """ def info(item) do :ets.info(@cache_name, item) end @doc """ Delete objects from the ETS table using a match specification. ## Examples iex> #{inspect(__MODULE__)}.select_delete([{{:key, :_}, [], [true]}]) 42 """ def select_delete(match_spec) do :ets.select_delete(@cache_name, match_spec) end @doc """ Delete objects from the ETS table that match the given pattern. ## Examples iex> #{inspect(__MODULE__)}.match_delete({:key, :_}) true """ def match_delete(pattern) do :ets.match_delete(@cache_name, pattern) end @doc """ Update a counter in the ETS table. ## Examples iex> #{inspect(__MODULE__)}.update_counter(:counter_key, {2, 1}) 43 """ def update_counter(key, update_op) do :ets.update_counter(@cache_name, key, update_op) end @doc """ Insert raw data into the ETS table using the underlying :ets.insert/2 function. ## Examples iex> #{inspect(__MODULE__)}.insert_raw({:key, "value"}) true iex> #{inspect(__MODULE__)}.insert_raw([{:key1, "value1"}, {:key2, "value2"}]) true """ def insert_raw(data) do :ets.insert(@cache_name, data) end if function_exported?(:ets, :to_dets, 1) do @doc """ Convert an ETS table to a DETS table. ## Examples iex> #{inspect(__MODULE__)}.to_dets(:my_dets_table) :ok """ def to_dets(dets_table) do :ets.to_dets(@cache_name, dets_table) end @doc """ Convert a DETS table to an ETS table. ## Examples iex> #{inspect(__MODULE__)}.from_dets(:my_dets_table) :ok """ def from_dets(dets_table) do :ets.from_dets(@cache_name, dets_table) end end end end @impl Cache def opts_definition, do: @opts_definition @impl Cache def start_link(opts) do Task.start_link(fn -> table_name = opts[:table_name] opts = opts |> Keyword.drop([:table_name, :type]) |> Kernel.++([opts[:type], :public, :named_table]) opts = if opts[:compressed] do Keyword.delete(opts, :compressed) ++ [:compressed] else opts end _ = :ets.new(table_name, opts) Process.hibernate(Function, :identity, [nil]) end) end @impl Cache def child_spec({cache_name, opts}) do %{ id: "#{cache_name}_elixir_cache_ets", start: {Cache.ETS, :start_link, [Keyword.put(opts, :table_name, cache_name)]} } end @impl Cache def get(cache_name, key, _opts \\ []) do case :ets.lookup(cache_name, key) do [{^key, value}] -> {:ok, value} # This can happen if someone uses insert_raw [value] -> {:ok, value} [] -> {:ok, nil} end end @impl Cache def put(cache_name, key, _ttl \\ nil, value, _opts \\ []) do :ets.insert(cache_name, {key, value}) :ok end @impl Cache def delete(cache_name, key, _opts \\ []) do :ets.delete(cache_name, key) :ok end end