%%%-------------------------------------------------------------------
%%% @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.