Packages

ETS backend for kura

Current section

Files

Jump to
kura_ets src kura_ets_pool.erl
Raw

src/kura_ets_pool.erl

-module(kura_ets_pool).
-moduledoc """
ETS pool implementation. There are no connections to pool: the "pool"
is a table registry, one gen_server per pool that owns all the ETS
data tables so they survive the (arbitrary, short-lived) processes
that read and write through the repo API.
## Layout
- One gen_server registered under the pool name. It owns every table.
- One named, protected meta table (named after the pool atom) mapping
`TableName :: binary()` to the data table tid. Callers resolve
tables with a plain `ets:lookup/2`; only creation goes through the
server, which serializes concurrent create races.
- One `set, public` data table per source table, created on demand at
first use (mirroring etso's TableServer-per-schema design).
`checkout/2` hands back the pool name itself: reads and writes go
straight to the public data tables, so there is nothing to lease.
## Example
```erlang
{ok, _Pid} = kura_ets_pool:start_pool(my_pool, #{}),
{ok, Tid} = kura_ets_pool:ensure_table(my_pool, ~"users"),
ok = kura_ets_pool:reset(my_pool),
ok = kura_ets_pool:stop_pool(my_pool).
```
""".
-behaviour(kura_pool).
-behaviour(kura_capabilities).
-behaviour(gen_server).
-export([
start_pool/2,
stop_pool/1,
checkout/2,
checkin/2,
give_away/3,
capabilities/0
]).
-export([ensure_table/2, reset/1]).
-export([init/1, handle_call/3, handle_cast/2]).
%%----------------------------------------------------------------------
%% kura_pool callbacks
%%----------------------------------------------------------------------
-spec start_pool(kura_pool:name(), kura_pool:opts()) -> {ok, pid()} | {error, term()}.
start_pool(Name, _Opts) ->
%% Unlinked start: the caller is kura's application master, which
%% must not be linked to (and killed with) a pool process.
gen_server:start({local, Name}, ?MODULE, Name, []).
-spec stop_pool(kura_pool:name()) -> ok.
stop_pool(Name) ->
case erlang:whereis(Name) of
undefined -> ok;
Pid -> gen_server:stop(Pid)
end.
-spec checkout(kura_pool:name(), kura_pool:checkout_opts()) ->
{ok, kura_pool:conn(), kura_pool:token()} | {error, term()}.
checkout(Name, _Opts) ->
case erlang:whereis(Name) of
undefined -> {error, no_pool};
_Pid -> {ok, Name, Name}
end.
-spec checkin(kura_pool:name(), kura_pool:token()) -> ok.
checkin(_Name, _Token) ->
ok.
-spec give_away(kura_pool:token(), pid(), term()) -> ok | {error, term()}.
give_away(_Token, NewOwner, _GiftData) when is_pid(NewOwner) ->
%% Nothing per-conn to transfer; the checkin-after-transfer
%% guarantee holds trivially because checkin is a no-op.
ok;
give_away(_Token, _NotPid, _GiftData) ->
{error, badarg}.
%%----------------------------------------------------------------------
%% kura_capabilities
%%----------------------------------------------------------------------
-doc """
The ETS backend returns the affected row from insert/update/delete
(the moral equivalent of `RETURNING *`), and nothing else from the
standard SQL capability set. Notably absent: `transactions` — writes
are applied immediately with no rollback.
""".
-spec capabilities() -> kura_capabilities:capability_set().
capabilities() ->
[returning].
%%----------------------------------------------------------------------
%% Table registry API (used by kura_ets_query)
%%----------------------------------------------------------------------
-doc "Return the data table for `TableName`, creating it on first use.".
-spec ensure_table(kura_pool:name(), binary()) -> {ok, ets:tid()} | {error, term()}.
ensure_table(Pool, TableName) when is_binary(TableName) ->
case ets:whereis(Pool) of
undefined ->
{error, no_pool};
_Meta ->
case ets:lookup(Pool, TableName) of
[{_, Tid}] -> {ok, Tid};
[] -> gen_server:call(Pool, {ensure_table, TableName})
end
end.
-doc "Delete all rows from every table in the pool. Handy between tests.".
-spec reset(kura_pool:name()) -> ok | {error, term()}.
reset(Pool) ->
case erlang:whereis(Pool) of
undefined -> {error, no_pool};
_Pid -> gen_server:call(Pool, reset)
end.
%%----------------------------------------------------------------------
%% gen_server callbacks
%%----------------------------------------------------------------------
-spec init(atom()) -> {ok, atom()}.
init(Name) ->
_Meta = ets:new(Name, [named_table, protected, set]),
{ok, Name}.
-spec handle_call(term(), gen_server:from(), atom()) -> {reply, term(), atom()}.
handle_call({ensure_table, TableName}, _From, Name) ->
case ets:lookup(Name, TableName) of
[{_, Tid}] ->
{reply, {ok, Tid}, Name};
[] ->
Tid = ets:new(kura_ets_table, [set, public]),
true = ets:insert(Name, {TableName, Tid}),
{reply, {ok, Tid}, Name}
end;
handle_call(reset, _From, Name) ->
lists:foreach(
fun({_TableName, Tid}) -> true = ets:delete_all_objects(Tid) end,
ets:tab2list(Name)
),
{reply, ok, Name};
handle_call(Request, _From, Name) ->
{reply, {error, {unknown_call, Request}}, Name}.
-spec handle_cast(term(), atom()) -> {noreply, atom()}.
handle_cast(_Msg, Name) ->
{noreply, Name}.