%%%------------------------------------------------------------------- %%% @doc %%% This module encapsulates the partitioned table metadata. %%% %%% Different properties must be stored somewhere so `shards' %%% can work properly. Shards perform logic on top of ETS tables, %%% for example, compute the partition based on the `Key' where %%% the action will be applied. To do so, it needs the number of %%% partitions, the function to select the partition, and also the %%% partition identifier to perform the ETS action. %%% @end %%%------------------------------------------------------------------- -module(shards_meta). %% API -export([ new/0, from_map/1, to_map/1, is_metadata/1, init/2, rename/2, lookup/2, put/3, get/1, get/2, get/3, get_partition_tids/1, get_partition_pids/1 ]). %% API – Getters -export([ tab_pid/1, keypos/1, partitions/1, keyslot_fun/1, parallel/1, parallel_timeout/1, ets_opts/1 ]). %% Inline-compiled functions -compile({inline, [ lookup/2, put/3, get/1, get_partition_tids/1, get_partition_pids/1 ]}). %%%=================================================================== %%% Types & Macros %%%=================================================================== %% Default number of partitions -define(PARTITIONS, erlang:system_info(schedulers_online)). %% Defines a tuple-list with the partition number and the ETS TID. -type partition_tids() :: [{non_neg_integer(), ets:tid()}]. %% Defines a tuple-list with the partition number and the partition owner PID. -type partition_pids() :: [{non_neg_integer(), pid()}]. %% Defines spec function to pick or compute the partition and/or node. %% The function returns a value for `Key' within the range `0..Range-1'. -type keyslot_fun() :: fun((Key :: term(), Range :: pos_integer()) -> non_neg_integer()). %% Metadata definition -record(meta, { tab_pid = undefined :: pid() | undefined, keypos = 1 :: pos_integer(), partitions = ?PARTITIONS :: pos_integer(), keyslot_fun = fun erlang:phash2/2 :: keyslot_fun(), parallel = false :: boolean(), parallel_timeout = infinity :: timeout(), ets_opts = [] :: [term()] }). %% Defines `shards' metadata. -type t() :: #meta{}. %% Defines the map representation for the metadata data type. -type meta_map() :: #{ tab_pid => pid(), keypos => pos_integer(), partitions => pos_integer(), keyslot_fun => keyslot_fun(), parallel => boolean(), parallel_timeout => timeout(), ets_opts => [term()] }. %% Exported types -export_type([ t/0, keyslot_fun/0, meta_map/0 ]). %%%=================================================================== %%% API %%%=================================================================== %% @doc %% Returns a metadata data type with the default values. %% @end -spec new() -> t(). new() -> #meta{}. %% @doc %% Builds a new `meta' from the given `Map'. %% @end -spec from_map(Map :: #{atom() => term()}) -> t(). from_map(Map) -> #meta{ tab_pid = maps:get(tab_pid, Map, self()), keypos = maps:get(keypos, Map, 1), partitions = maps:get(partitions, Map, ?PARTITIONS), keyslot_fun = maps:get(keyslot_fun, Map, fun erlang:phash2/2), parallel = maps:get(parallel, Map, false), parallel_timeout = maps:get(parallel_timeout, Map, infinity), ets_opts = maps:get(ets_opts, Map, []) }. %% @doc %% Converts the given `Meta' into a `map'. %% @end -spec to_map(t()) -> meta_map(). to_map(Meta) -> #{ tab_pid => Meta#meta.tab_pid, keypos => Meta#meta.keypos, partitions => Meta#meta.partitions, keyslot_fun => Meta#meta.keyslot_fun, parallel => Meta#meta.parallel, parallel_timeout => Meta#meta.parallel_timeout, ets_opts => Meta#meta.ets_opts }. %% @doc %% Returns `true' if `Meta' is a metadata data type, otherwise, %% `false' is returned. %% @end -spec is_metadata(Meta :: term()) -> boolean(). is_metadata(#meta{}) -> true; is_metadata(_) -> false. %% @doc %% Initializes the metadata ETS table. %% @end -spec init(Name, Opts) -> Tab when Name :: atom(), Opts :: [shards:option()], Tab :: shards:tab(). init(Tab, Opts) -> ExtraOpts = case lists:member(named_table, Opts) of true -> [named_table]; false -> [] end, ets:new(Tab, [set, public, {read_concurrency, true}] ++ ExtraOpts). %% @doc %% Renames the metadata ETS table. %% @end -spec rename(Tab, Name) -> Name when Tab :: shards:tab(), Name :: atom(). rename(Tab, Name) -> ets:rename(Tab, Name). %% @doc %% Returns the value associated to the key `Key' in the metadata table `Tab'. %% If `Key' is not found, the error `{unknown_table, Tab}' is raised. %% @end -spec lookup(Tab, Key) -> term() when Tab :: shards:tab(), Key :: term(). lookup(Tab, Key) -> try ets:lookup_element(Tab, Key, 2) catch error:badarg -> error({unknown_table, Tab}) end. %% @doc %% Stores the value `Val' under the given key `Key' into the metadata table %% `Tab'. %% @end -spec put(Tab, Key, Val) -> ok when Tab :: shards:tab(), Key :: term(), Val :: term(). put(Tab, Key, Val) -> true = ets:insert(Tab, {Key, Val}), ok. %% @doc %% Returns the `tab_info' within the metadata. %% @end -spec get(Tab :: shards:tab()) -> t() | no_return(). get(Tab) -> lookup(Tab, '$tab_info'). %% @equiv get(Tab, Key, undefined) get(Tab, Key) -> get(Tab, Key, undefined). %% @doc %% Returns the value for the given `Key' in the metadata, %% or `Def' if `Key' is not set. %% @end -spec get(Tab, Key, Def) -> Val when Tab :: shards:tab(), Key :: term(), Def :: term(), Val :: term(). get(Tab, Key, Def) -> try case ets:lookup(Tab, Key) of [{Key, Val}] -> Val; [] -> Def end catch error:badarg -> error({unknown_table, Tab}) end. %% @doc %% Returns a list with the partition TIDs. %% @end -spec get_partition_tids(Tab :: shards:tab()) -> partition_tids(). get_partition_tids(Tab) -> partitions_info(Tab, tid). %% @doc %% Returns a list with the partition PIDs. %% @end -spec get_partition_pids(Tab :: shards:tab()) -> partition_pids(). get_partition_pids(Tab) -> partitions_info(Tab, pid). %% @private partitions_info(Tab, KeyPrefix) -> try ets:select(Tab, [{{{KeyPrefix, '$1'}, '$2'}, [], [{{'$1', '$2'}}]}]) catch error:badarg -> error({unknown_table, Tab}) end. %%%=================================================================== %%% Getters %%%=================================================================== -spec tab_pid(t() | shards:tab()) -> pid(). tab_pid(#meta{tab_pid = Value}) -> Value; tab_pid(Tab) when is_atom(Tab); is_reference(Tab) -> tab_pid(?MODULE:get(Tab)). -spec keypos(t() | shards:tab()) -> pos_integer(). keypos(#meta{keypos = Value}) -> Value; keypos(Tab) when is_atom(Tab); is_reference(Tab) -> keypos(?MODULE:get(Tab)). -spec partitions(t() | shards:tab()) -> pos_integer(). partitions(#meta{partitions = Value}) -> Value; partitions(Tab) when is_atom(Tab); is_reference(Tab) -> partitions(?MODULE:get(Tab)). -spec keyslot_fun(t() | shards:tab()) -> keyslot_fun(). keyslot_fun(#meta{keyslot_fun = Value}) -> Value; keyslot_fun(Tab) when is_atom(Tab); is_reference(Tab) -> keyslot_fun(?MODULE:get(Tab)). -spec parallel(t() | shards:tab()) -> boolean(). parallel(#meta{parallel = Value}) -> Value; parallel(Tab) when is_atom(Tab); is_reference(Tab) -> parallel(?MODULE:get(Tab)). -spec parallel_timeout(t() | shards:tab()) -> timeout(). parallel_timeout(#meta{parallel_timeout = Value}) -> Value; parallel_timeout(Tab) when is_atom(Tab); is_reference(Tab) -> parallel_timeout(?MODULE:get(Tab)). -spec ets_opts(t() | shards:tab()) -> [term()]. ets_opts(#meta{ets_opts = Value}) -> Value; ets_opts(Tab) when is_atom(Tab); is_reference(Tab) -> ets_opts(?MODULE:get(Tab)).