Current section

Files

Jump to
launchdarkly_server_sdk src ldclient_storage_ets_server.erl
Raw

src/ldclient_storage_ets_server.erl

%%-------------------------------------------------------------------
%% @doc `ldclient_storage_ets_server' module
%% @private
%% This is a simple in-memory implementation of an storage server using ETS.
%% @end
%%-------------------------------------------------------------------
-module(ldclient_storage_ets_server).
-behaviour(gen_server).
%% Supervision
-export([start_link/2, init/1]).
%% Behavior callbacks
-export([code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
%% API
-export([create/2]).
-export([empty/2]).
-export([get/3]).
-export([all/2]).
-export([upsert/3]).
-export([upsert_clean/3]).
-export([delete/3]).
%% Types
-type state() :: #{
tids => map(),
tag => atom()
}.
%%===================================================================
%% Supervision
%%===================================================================
start_link(WorkerRegName, Tag) ->
error_logger:info_msg("Starting ets server with name ~p", [WorkerRegName]),
gen_server:start_link({local, WorkerRegName}, ?MODULE, [Tag], []).
init([Tag]) ->
true = ldclient_update_processor_state:set_storage_initialized_state(Tag, true),
{ok, #{tids => #{}, tag => Tag}}.
%%===================================================================
%% API
%%===================================================================
%% @doc Create a bucket
%%
%% @end
-spec create(ServerRef :: atom(), Bucket :: atom()) ->
ok |
{error, already_exists, string()}.
create(ServerRef, Bucket) when is_atom(Bucket) ->
gen_server:call(ServerRef, {create, Bucket}).
%% @doc Empty a bucket
%%
%% @end
-spec empty(ServerRef :: atom(), Bucket :: atom()) ->
ok |
{error, bucket_not_found, string()}.
empty(ServerRef, Bucket) when is_atom(Bucket) ->
gen_server:call(ServerRef, {empty, Bucket}).
%% @doc Get an item from the bucket by its key
%%
%% @end
-spec get(ServerRef :: atom(), Bucket :: atom(), Key :: binary()) ->
[{Key :: binary(), Value :: any()}] |
{error, bucket_not_found, string()}.
get(ServerRef, Bucket, Key) when is_atom(Bucket), is_binary(Key) ->
gen_server:call(ServerRef, {get, Bucket, Key}).
%% @doc List all items in a bucket
%%
%% @end
-spec all(ServerRef :: atom(), Bucket :: atom()) ->
[{Key :: binary(), Value :: any()}] |
{error, bucket_not_found, string()}.
all(ServerRef, Bucket) when is_atom(Bucket) ->
gen_server:call(ServerRef, {all, Bucket}).
%% @doc Upsert item key value pairs in an existing bucket
%%
%% @end
-spec upsert(ServerRef :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) ->
ok |
{error, bucket_not_found, string()}.
upsert(ServerRef, Bucket, Items) when is_atom(Bucket), is_map(Items) ->
ok = gen_server:call(ServerRef, {upsert, Bucket, Items}).
%% @doc Perform an atomic empty and upsert
%%
%% @end
-spec upsert_clean(ServerRef :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) ->
ok |
{error, bucket_not_found, string()}.
upsert_clean(ServerRef, Bucket, Items) when is_atom(Bucket), is_map(Items) ->
ok = gen_server:call(ServerRef, {upsert_clean, Bucket, Items}).
%% @doc Delete an item from the bucket by its key
%%
%% @end
-spec delete(ServerRef :: atom(), Bucket :: atom(), Key :: binary()) ->
ok |
{error, bucket_not_found, string()}.
delete(ServerRef, Bucket, Key) ->
ok = gen_server:call(ServerRef, {delete, Bucket, Key}).
%%===================================================================
%% Behavior callbacks
%%===================================================================
-type from() :: {pid(), term()}.
-spec handle_call(term(), from(), state()) -> {reply, term(), state()}.
handle_call({create, Bucket}, _From, #{tids := Tids} = State) ->
{Reply, NewTids} = create_bucket(bucket_exists(Bucket, Tids), Bucket, Tids),
NewState = State#{tids := NewTids},
{reply, Reply, NewState};
handle_call({empty, Bucket}, _From, #{tids := Tids} = State) ->
{reply, empty_bucket(bucket_exists(Bucket, Tids), Bucket, Tids), State};
handle_call({get, Bucket, Key}, _From, #{tids := Tids} = State) ->
{reply, lookup_key(bucket_exists(Bucket, Tids), Key, Bucket, Tids), State};
handle_call({all, Bucket}, _From, #{tids := Tids} = State) ->
{reply, all_items(bucket_exists(Bucket, Tids), Bucket, Tids), State};
handle_call({upsert, Bucket, Item}, _From, #{tids := Tids} = State) ->
{reply, upsert_items(bucket_exists(Bucket, Tids), Item, Bucket, Tids), State};
handle_call({upsert_clean, Bucket, Item}, _From, #{tids := Tids} = State) ->
{reply, upsert_clean_items(bucket_exists(Bucket, Tids), Item, Bucket, Tids), State};
handle_call({delete, Bucket, Key}, _From, #{tids := Tids} = State) ->
{reply, delete_key(bucket_exists(Bucket, Tids), Key, Bucket, Tids), State}.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Msg, State) -> {noreply, State}.
terminate(_Reason, #{tag := Tag} = _State) ->
true = ldclient_update_processor_state:set_storage_initialized_state(Tag, reload),
ok.
code_change(_OldVersion, State, _Extra) -> {ok, State}.
%%===================================================================
%% Internal functions
%%===================================================================
%% @doc Check whether bucket already exists
%% @private
%%
%% @end
-spec bucket_exists(Bucket :: atom(), Tids :: map()) -> boolean().
bucket_exists(Bucket, Tids) when is_atom(Bucket) ->
maps:is_key(Bucket, Tids).
%% @doc Create a bucket
%% @private
%%
%% If bucket with the same name already exists an error will be returned.
%% @end
-spec create_bucket(BucketExists :: boolean(), Bucket :: atom(), Tids :: map()) ->
{ok|{error, already_exists, string()}, NewTids :: map()}.
create_bucket(true, Bucket, Tids) ->
{{error, already_exists, "ETS table " ++ atom_to_list(Bucket) ++ " already exists."}, Tids};
create_bucket(false, Bucket, Tids) ->
Tid = ets:new(Bucket, [set]),
{ok, Tids#{Bucket => Tid}}.
%% @doc Empty a bucket
%% @private
%%
%% If no such bucket exists, error will be returned.
%% @end
-spec empty_bucket(BucketExists :: boolean(), Bucket :: atom(), Tids :: map()) ->
ok |
{error, bucket_not_found, string()}.
empty_bucket(false, Bucket, _Tids) ->
{error, bucket_not_found, "ETS table " ++ atom_to_list(Bucket) ++ " does not exist."};
empty_bucket(true, Bucket, Tids) ->
Tid = maps:get(Bucket, Tids),
true = ets:delete_all_objects(Tid),
ok.
%% @doc List all items in a bucket
%% @private
%%
%% Returns all items stored in the bucket.
%% @end
-spec all_items(BucketExists :: boolean(), Bucket :: atom(), Tids :: map()) ->
[tuple()] |
{error, bucket_not_found, string()}.
all_items(false, Bucket, _Tids) ->
{error, bucket_not_found, "ETS table " ++ atom_to_list(Bucket) ++ " does not exist."};
all_items(true, Bucket, Tids) ->
Tid = maps:get(Bucket, Tids),
ets:tab2list(Tid).
%% @doc Look up an item by key
%% @private
%%
%% Search for an item by its key.
%% @end
-spec lookup_key(BucketExists :: boolean(), Key :: binary(), Bucket :: atom(), Tids :: map()) ->
[tuple()] |
{error, bucket_not_found, string()}.
lookup_key(false, _Key, Bucket, _Tids) ->
{error, bucket_not_found, "ETS table " ++ atom_to_list(Bucket) ++ " does not exist."};
lookup_key(true, Key, Bucket, Tids) ->
Tid = maps:get(Bucket, Tids),
ets:lookup(Tid, Key).
%% @doc Upsert key value pairs in bucket
%% @private
%%
%% @end
-spec upsert_items(BucketExists :: boolean(), Items :: #{Key :: binary() => Value :: any()}, Bucket :: atom(), Tids :: map()) ->
ok |
{error, bucket_not_found, string()}.
upsert_items(false, _Items, Bucket, _Tids) ->
{error, bucket_not_found, "ETS table " ++ atom_to_list(Bucket) ++ " does not exist."};
upsert_items(true, Items, Bucket, Tids) ->
Tid = maps:get(Bucket, Tids),
true = ets:insert(Tid, maps:to_list(Items)),
ok.
%% @doc Empty bucket and upsert key value pairs
%% @private
%%
%% @end
-spec upsert_clean_items(BucketExists :: boolean(), Items :: #{Key :: binary() => Value :: any()}, Bucket :: atom(), Tids :: map()) ->
ok |
{error, bucket_not_found, string()}.
upsert_clean_items(false, _Items, Bucket, _Tids) ->
{error, bucket_not_found, "ETS table " ++ atom_to_list(Bucket) ++ " does not exist."};
upsert_clean_items(true, Items, Bucket, Tids) ->
Tid = maps:get(Bucket, Tids),
true = ets:delete_all_objects(Tid),
true = ets:insert(Tid, maps:to_list(Items)),
ok.
%% @doc Delete an item by key
%% @private
%%
%% Delete an item by its key.
%% @end
-spec delete_key(BucketExists :: boolean(), Key :: binary(), Bucket :: atom(), Tids :: map()) ->
ok |
{error, bucket_not_found, string()}.
delete_key(false, _Key, Bucket, _Tids) ->
{error, bucket_not_found, "ETS table " ++ atom_to_list(Bucket) ++ " does not exist."};
delete_key(true, Key, Bucket, Tids) ->
Tid = maps:get(Bucket, Tids),
true = ets:delete(Tid, Key),
ok.