src/erllama_cache_ram.erl

%% Copyright (c) 2026 Benoit Chesneau. Licensed under the MIT License.
%% See the LICENSE file at the project root.
%%
-module(erllama_cache_ram).
-moduledoc """
RAM tier slab store.

Owns the `erllama_cache_ram_slabs` ETS table. Reads and deletes
are direct ETS operations from any process; puts go through this
module's `put/3` because they need to atomically (a) insert the
slab and (b) register the meta row via the meta server.

Slab binaries above 64 bytes live off-heap as refcounted ProcBins
so `ets:lookup` returns a binary reference, not a copy. There is
no slab fragmentation to worry about; eviction frees the slab
binary by deleting its row.
""".
-behaviour(gen_server).

-export([start_link/0, put/3, load/1, delete/1, size_bytes/0]).
-export([init/1, handle_call/3, handle_cast/2]).

-define(SLABS, erllama_cache_ram_slabs).
-define(SERVER, ?MODULE).

%% =============================================================================
%% Public API
%% =============================================================================

-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

-spec put(erllama_cache:cache_key(), binary(), binary()) -> ok.
put(Key, Slab, Header) when is_binary(Slab), is_binary(Header) ->
    ets:insert(?SLABS, {Key, Slab}),
    erllama_cache_meta_srv:insert_available(
        Key, ram, byte_size(Slab), Header, {ram}
    ).

-spec load(erllama_cache:cache_key()) -> {ok, binary()} | miss.
load(Key) ->
    case ets:lookup(?SLABS, Key) of
        [{_, Slab}] -> {ok, Slab};
        [] -> miss
    end.

-spec delete(erllama_cache:cache_key()) -> ok.
delete(Key) ->
    %% Idempotent against a non-existent table; the ram tier may not
    %% have been started yet (early init) or may have crashed and not
    %% yet been restarted.
    try
        ets:delete(?SLABS, Key),
        ok
    catch
        error:badarg -> ok
    end.

-spec size_bytes() -> non_neg_integer().
size_bytes() ->
    ets:foldl(fun({_, S}, Acc) -> Acc + byte_size(S) end, 0, ?SLABS).

%% =============================================================================
%% gen_server callbacks
%% =============================================================================

-spec init([]) -> {ok, []}.
init([]) ->
    ets:new(?SLABS, [
        named_table,
        public,
        set,
        {read_concurrency, true},
        {write_concurrency, true}
    ]),
    {ok, []}.

handle_call(_Msg, _From, S) ->
    {reply, {error, unknown_call}, S}.

-spec handle_cast(term(), term()) -> {noreply, term()}.
handle_cast(_Msg, S) ->
    {noreply, S}.