%%%------------------------------------------------------------------- %%% @author Fred Youhanaie %%% @copyright (C) 2019, Fred Youhanaie %%% @doc %%% %%% A set of utility functions to support the rest of the espace %%% modules. %%% %%% When `espace' instances are started, a number of instance specific %%% `persistent_term' entries are created. The `pterm_*' functions are %%% used for handling these terms. %%% %%% The `persistent_term' entries are expected to have the following %%% format: `{espace, Inst_name, Key}', where, `Key' identifies the %%% particular `espace' item, such as server name to ETS table name, %%% and `Inst_name' is the espace instance name, which is the same as %%% the application name. For the unnamed instance the `Inst_name' is %%% `espace'. %%% %%% @end %%% Created : 11 Mar 2019 by Fred Youhanaie %%%------------------------------------------------------------------- -module(espace_util). %% API -export([eval_out/1, eval_out/2]). -export([inst_to_name/2, wait4etsmgr/4]). -export([pterm_erase/1, pterm_get/2, pterm_put/3]). -export([opcount_new/0, opcount_incr/1, opcount_counts/0, opcount_reset/0]). -export([opcount_new/1, opcount_incr/2, opcount_counts/1, opcount_reset/1]). %%-------------------------------------------------------------------- -type(pt_key() :: {espace, atom(), atom()}). %% used for locating all keys -define(PTerm_key(Inst_name), {espace, Inst_name, _}). %% used for locating specific keys -define(PTerm_key(Inst_name, Key), {espace, Inst_name, Key}). %%-------------------------------------------------------------------- -record(opctr, {in, rd, inp, rdp, out, eval}). %% the counter elements by name -define(Opctr_names, record_info(fields, opctr)). -define(Opctr_size, record_info(size, opctr)). -define(Opctr_ref, pterm_get(Inst_name, opcounters)). -type(espace_op() :: in | rd | inp | rdp | out | eval). %%%=================================================================== %%% API %%%=================================================================== %%-------------------------------------------------------------------- %% @doc %% Convert an instance name to longer prefixed name. %% %% This is used for obtaining the instance specific server/table %% names. For example `inst_to_name(espace_sup, aaa)' will return %% `espace_sup_aaa'. %% %% If the instance name is `espace', then the prefix is returned %% without an instance name suffix. For example %% `inst_to_name(espace_sup, espace)' will return `espace_sup'. %% %% Instead of generating the full name each time this function is %% called, we perform the conversion once and cache the result as an %% entry in the `persistent_term' store. Each entry has the key %% `{espace, Inst_name, Prefix}', e.g. `{espace, aaa, espace_sup}', %% and the full name as value, e.g. `espace_sup_aaa'. %% %% The use of `persistent_term' store will help speed up all named %% instances, including the short lived `eval/2' processes. %% %% @end %%-------------------------------------------------------------------- -spec inst_to_name(atom(), atom()) -> atom(). inst_to_name(Prefix, Inst_name) -> case pterm_get(Inst_name, Prefix) of undefined -> V = case Inst_name of espace -> Prefix; _ -> list_to_atom(atom_to_list(Prefix) ++ "_" ++ atom_to_list(Inst_name)) end, pterm_put(Inst_name, Prefix, V), V; V -> V end. %%-------------------------------------------------------------------- %% @doc Lookup the instance `Key' for `Inst_name'. %% %% If the persistence term does not exist, `undefined' is returned. %% %% @end %%-------------------------------------------------------------------- -spec pterm_get(atom(), atom()) -> undefined | term(). pterm_get(Inst_name, Key) -> PT_key = ?PTerm_key(Inst_name, Key), persistent_term:get(PT_key, undefined). %%-------------------------------------------------------------------- %% @doc Create an `espace' persistent term. %% %% @end %%-------------------------------------------------------------------- -spec pterm_put(atom(), atom(), term()) -> ok. pterm_put(Inst_name, Key, Term) -> PT_key = ?PTerm_key(Inst_name, Key), persistent_term:put(PT_key, Term). %%-------------------------------------------------------------------- %% @doc Remove all the persistent terms for a given `espace' instance. %% %% @end %%-------------------------------------------------------------------- -spec pterm_erase(atom()) -> [] | [{pt_key(), term()}]. pterm_erase(Inst_name) -> Erase_term = fun ({K = ?PTerm_key(Inst), V}) when Inst == Inst_name -> case persistent_term:erase(K) of true -> {true, {K, V}}; false -> false end; (_) -> false end, Pterms = persistent_term:get(), lists:filtermap(Erase_term, Pterms). %%-------------------------------------------------------------------- %% @doc Conditionally evaluate a tuple and `out' the result to the %% unnamed instance. %% %% See `eval_out/2' for details. %% %% @end %%-------------------------------------------------------------------- -spec eval_out(tuple()) -> done. eval_out(Tuple_in) -> eval_out(espace, Tuple_in). %%-------------------------------------------------------------------- %% @doc Conditionally evaluate a tuple and `out' the result to a named %% instance. %% %% The elements of the output tuple correspond to those of %% `Tuple_in'. If any of the elements of `Tuple_in' are recognized as %% function, then the corresponding output element will be the value %% of the function. %% %% Two types of patterns are recognized as functions and are %% evaluated. A normal function expression of arity zero, `fun () -> %% expr end'. And, a tuple with two elements, a function expresion of %% arity `N' and a list of length `N', `N' can be zero. %% %% Any other pattern will move the element to the output tuple %% untouched. %% %% @end %%-------------------------------------------------------------------- -spec eval_out(atom(), tuple()) -> done. eval_out(Inst_name, Tuple_in) -> List_in = erlang:tuple_to_list(Tuple_in), List_out = lists:map(fun (X) -> do_eval(X) end, List_in), Tuple_out = erlang:list_to_tuple(List_out), espace:out(Inst_name, Tuple_out). %%-------------------------------------------------------------------- %% @doc wait for etsmgr to (re)start, then ask it to manage our table. %% %% There are two occassions where this function is called: %% %%
    %% %%
  1. `init' - start/restart of our gen_server that owns a table, in %% this case we do not have, or know of, the ETS table. So we ask %% `etsmgr' to create a new table using `etsmgr:new_table/4'. If %% `etsmgr' is already managing such a table that does not already %% belong to another process, then that table will be given to %% us.
  2. %% %%
  3. `recover' - recovery of the `etsmgr' server, in this case we %% ask `etsmgr' to start managing our ETS table.
  4. %% %%
%% %% @end %%-------------------------------------------------------------------- -spec wait4etsmgr(atom(), init | recover, atom(), term()) -> {ok, pid(), ets:tab()} | {error, term()}. wait4etsmgr(Inst_name, init, Table_name, Table_opts) -> etsmgr:wait4etsmgr(Inst_name), etsmgr:new_table(Inst_name, Table_name, Table_name, Table_opts); wait4etsmgr(Inst_name, recover, Table_name, Table_id) -> etsmgr:wait4etsmgr(Inst_name), etsmgr:add_table(Inst_name, Table_name, Table_id). %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% @doc Create a new ops counter array for the unnamed instance. %% %% See opcount_new/1 for details. %% %% @end %%-------------------------------------------------------------------- -spec opcount_new() -> ok. opcount_new() -> opcount_new(espace). %%-------------------------------------------------------------------- %% @doc Create a new ops counter array for a named instance. %% %% The array will have one counter per espace operation. The counters %% ref is saved in as a persistent term. %% %% @end %%-------------------------------------------------------------------- -spec opcount_new(atom()) -> ok. opcount_new(Inst_name) -> Ctr_ref = counters:new(?Opctr_size, []), pterm_put(Inst_name, opcounters, Ctr_ref). %%-------------------------------------------------------------------- %% @doc Increment a single espace op counter for the unnamed instance. %% %% See opcount_incr/1 for details. %% %% @end %%-------------------------------------------------------------------- -spec opcount_incr(espace_op()) -> ok. opcount_incr(Op) -> opcount_incr(espace, Op). %%-------------------------------------------------------------------- %% @doc Increment a single espace op counter for a named instance. %% %% In the interested of keeping the code simple, the counter index of %% each op corresponds to the position of `Op' in the record tuple, %% which ranges from 2 to 7. %% %% @end %%-------------------------------------------------------------------- -spec opcount_incr(Inst_name :: atom(), Op :: espace_op()) -> ok. opcount_incr(Inst_name, in) -> counters:add(?Opctr_ref, #opctr.in, 1); opcount_incr(Inst_name, rd) -> counters:add(?Opctr_ref, #opctr.rd, 1); opcount_incr(Inst_name, inp) -> counters:add(?Opctr_ref, #opctr.inp, 1); opcount_incr(Inst_name, rdp) -> counters:add(?Opctr_ref, #opctr.rdp, 1); opcount_incr(Inst_name, out) -> counters:add(?Opctr_ref, #opctr.out, 1); opcount_incr(Inst_name, eval) -> counters:add(?Opctr_ref, #opctr.eval, 1). %%-------------------------------------------------------------------- %% @doc Return the current counts for the unnamed instance as a map. %% %% See opcount_count/1 for details. %% %% @end %%-------------------------------------------------------------------- -spec opcount_counts() -> #{espace_op() => integer()}. opcount_counts() -> opcount_counts(espace). %%-------------------------------------------------------------------- %% @doc Return the current counts for a named instance as a map. %% %% @end %%-------------------------------------------------------------------- -spec opcount_counts(atom()) -> #{espace_op() => integer()}. opcount_counts(Inst_name) -> C = ?Opctr_ref, Counts = [ counters:get(C, Idx) || Idx <- lists:seq(2, ?Opctr_size) ], maps:from_list(lists:zip(?Opctr_names, Counts)). %%-------------------------------------------------------------------- %% @doc Reset all the op counters of the unnamed instance. %% %% See `opcount_reset/1' for details. %% %% @end %%-------------------------------------------------------------------- -spec opcount_reset() -> ok. opcount_reset() -> opcount_reset(espace). %%-------------------------------------------------------------------- %% @doc Reset all the op counters of a named instance. %% %% This function has been provided for investigating an application. %% %% @end %%-------------------------------------------------------------------- -spec opcount_reset(atom()) -> ok. opcount_reset(Inst_name) -> C = ?Opctr_ref, [ counters:put(C, Idx, 0) || Idx <- lists:seq(2, ?Opctr_size) ], ok. %%%=================================================================== %%% Internal functions %%%=================================================================== %%-------------------------------------------------------------------- %% @doc check and evaluate a term, if it is a function. %% %% @end %%-------------------------------------------------------------------- -spec do_eval(term()) -> term(). do_eval(Fun) when is_function(Fun, 0) -> erlang:apply(Fun, []); do_eval({Fun, Args}) when is_list(Args) andalso is_function(Fun, length(Args)) -> erlang:apply(Fun, Args); do_eval(X) -> X.