Skip to main content

src/barrel_cache.erl

%%%-------------------------------------------------------------------
%%% @doc Shared RocksDB block cache manager
%%%
%%% Manages a shared block cache used by all RocksDB instances.
%%% This improves memory efficiency and cache hit rates by sharing
%%% a single cache across document stores and attachment stores.
%%%
%%% The cache is configured via application environment:
%%% ```
%%% {barrel_docdb, [
%%%     {block_cache_size, 536870912},  %% 512MB default
%%%     {block_cache_type, clock}       %% clock (default) or lru
%%% ]}
%%% '''
%%%
%%% Clock cache provides better performance under concurrent load.
%%% @end
%%%-------------------------------------------------------------------
-module(barrel_cache).

-behaviour(gen_server).

%% API
-export([start_link/0]).
-export([get_cache/0]).
-export([get_block_opts/0]).
-export([get_block_opts/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2]).

-define(SERVER, ?MODULE).

-record(state, {
    cache :: rocksdb:cache_handle() | undefined
}).

%%====================================================================
%% API
%%====================================================================

%% @doc Start the cache manager
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%% @doc Get the shared cache handle
-spec get_cache() -> {ok, rocksdb:cache_handle()} | {error, not_started}.
get_cache() ->
    gen_server:call(?SERVER, get_cache).

%% @doc Get block-based table options with shared cache and bloom filter
-spec get_block_opts() -> list().
get_block_opts() ->
    get_block_opts(#{}).

%% @doc Get block-based table options with custom settings
%%
%% Options:
%% - `bloom_bits': Bloom filter bits per key (default: 10)
%% - `block_size': Block size in bytes (default: 4096)
%% - `cache_index_and_filter': Cache index/filter blocks (default: true)
%%
%% If the cache process is not running (e.g., during standalone tests),
%% returns options without the shared cache.
-spec get_block_opts(map()) -> list().
get_block_opts(Options) ->
    try
        gen_server:call(?SERVER, {get_block_opts, Options})
    catch
        exit:{noproc, _} ->
            %% Cache not started - return options without shared cache
            build_block_opts_no_cache(Options)
    end.

%% Build block options without shared cache (for standalone usage)
build_block_opts_no_cache(Options) ->
    BloomBits = maps:get(bloom_bits, Options, 10),
    BlockSize = maps:get(block_size, Options, 4096),
    CacheIndexAndFilter = maps:get(cache_index_and_filter, Options, true),
    [
        {filter_policy, {bloom_filter, BloomBits}},
        {whole_key_filtering, false},
        {block_size, BlockSize},
        {cache_index_and_filter_blocks, CacheIndexAndFilter}
    ].

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

init([]) ->
    CacheSize = application:get_env(barrel_docdb, block_cache_size, 512 * 1024 * 1024),
    CacheType = application:get_env(barrel_docdb, block_cache_type, clock),
    {ok, Cache} = rocksdb:new_cache(CacheType, CacheSize),
    logger:info("barrel_cache: initialized ~p MB ~p cache",
                       [CacheSize div (1024 * 1024), CacheType]),
    {ok, #state{cache = Cache}}.

handle_call(get_cache, _From, #state{cache = Cache} = State) ->
    case Cache of
        undefined -> {reply, {error, not_started}, State};
        _ -> {reply, {ok, Cache}, State}
    end;

handle_call({get_block_opts, Options}, _From, #state{cache = Cache} = State) ->
    BloomBits = maps:get(bloom_bits, Options, 10),
    BlockSize = maps:get(block_size, Options, 4096),
    CacheIndexAndFilter = maps:get(cache_index_and_filter, Options, true),

    BaseOpts = [
        {filter_policy, {bloom_filter, BloomBits}},
        {whole_key_filtering, false},
        {block_size, BlockSize},
        {cache_index_and_filter_blocks, CacheIndexAndFilter}
    ],

    BlockOpts = case Cache of
        undefined -> BaseOpts;
        _ -> [{block_cache, Cache} | BaseOpts]
    end,

    {reply, BlockOpts, State};

handle_call(_Request, _From, State) ->
    {reply, {error, unknown_request}, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    %% Cache is automatically released when process terminates
    ok.