%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2026, %%% @doc %%% DETS backed outbound packet cache for efka_iot_client. %%% @end %%%------------------------------------------------------------------- -module(efka_iot_cache). -author("anlicheng"). -define(CACHE_TAB, cache). -define(MAX_CACHE_ITEMS, 5000000). -define(MAX_CACHE_FILE_SIZE, 1073741824). -export([open/0, close/0, insert/1, fetch_next/0, delete/1]). -spec open() -> ok | {error, term()}. open() -> {ok, DetsDir} = application:get_env(efka, dets_dir), File = DetsDir ++ "cache.dets", case dets:open_file(?CACHE_TAB, [{file, File}, {type, bag}, {keypos, 1}]) of {ok, ?CACHE_TAB} -> ok; {error, Reason} -> {error, Reason} end. -spec close() -> ok. close() -> case dets:close(?CACHE_TAB) of ok -> ok; {error, not_owner} -> ok end. -spec insert(binary()) -> {ok, non_neg_integer()} | {error, term()}. insert(Data) when is_binary(Data) -> case dets:insert(?CACHE_TAB, {generate_cache_id(), Data}) of ok -> trim_limits(0); {error, Reason} -> {error, Reason} end. -spec fetch_next() -> error | {ok, {integer(), binary()}}. fetch_next() -> case dets:first(?CACHE_TAB) of '$end_of_table' -> error; Key -> case dets:lookup(?CACHE_TAB, Key) of [Entry | _] -> {ok, Entry}; [] -> fetch_next() end end. -spec delete(integer()) -> ok | {error, term()}. delete(Id) when is_integer(Id) -> dets:delete(?CACHE_TAB, Id). -spec generate_cache_id() -> integer(). generate_cache_id() -> erlang:unique_integer([monotonic, positive]). -spec over_limit() -> boolean(). over_limit() -> item_count() > ?MAX_CACHE_ITEMS orelse file_size() > ?MAX_CACHE_FILE_SIZE. -spec item_count() -> non_neg_integer(). item_count() -> dets:info(?CACHE_TAB, size). -spec file_size() -> non_neg_integer(). file_size() -> dets:info(?CACHE_TAB, file_size). -spec trim_limits(non_neg_integer()) -> {ok, non_neg_integer()} | {error, term()}. trim_limits(DroppedCount) -> case over_limit() of true -> case delete_oldest_entry() of ok -> trim_limits(DroppedCount + 1); error -> {ok, DroppedCount}; {error, Reason} -> {error, Reason} end; false -> {ok, DroppedCount} end. -spec delete_oldest_entry() -> ok | error | {error, term()}. delete_oldest_entry() -> case dets:first(?CACHE_TAB) of '$end_of_table' -> error; Key -> dets:delete(?CACHE_TAB, Key) end.